CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
weave's tracker (the Development space in the weave workspace) is the changelog of record: every Feature and Issue row carries its evidence. This file is the release-notes digest.
## v0.4.132 (2026-11-10)
- On a touch screen the date and range pickers give every control a 44 px hit area: the month lays out as a 7 × 44 px grid in a 336 px popover, and the month arrows, the month and year buttons, Clear, Today and the typed date inputs each take a 44 px box. The phone touch gate now walks both pickers, so a smaller control there fails it ([[Issue#1184]]).
- On a touch screen the icon picker draws its cells on a 44 px grid, eight to a row on a phone, and its search field takes a 44 px box. The cell is drawn at 46 px so that 44 px of it is still reachable under WebKit's coarser hit test on iOS. The phone touch gate now walks the icon picker, so a smaller cell there fails it ([[Issue#1189]]).
- Opening a record whose HTML introduction sits above its fields no longer shows the fields and then shoves them off screen when the introduction loads. The fields and everything below the introduction wait until the frame reports its height, the last measured height of each introduction is reserved on the next open so it paints in place at once, and the editor's Introduction options decide their visibility once per session instead of growing the block a beat later. A new `html-intro-row` flicker journey opens such a record beside the table and holds the fix in `test/flicker-fixed.json` ([[Issue#1552]]).
- On a touch screen the multi-select picker's **Done** button takes a 46 px box, so the full 44 px is reachable on both engines. The phone touch gate now walks the multi-select picker, so a smaller control in its footer fails it ([[Issue#1226]]).
- An observer opening a record sees its title, field values and document body read-only, with no drag grips, field dialogs, "Make a field" button or next-state button. The record read now carries `readOnly: true` when the viewer's role in that space is below editor, and the record page gates every write control on it. Comments stay open to observers (Issue #1537).
- A hosted weave instance now logs the layout shifts and long animation frames its users hit. `public/paint.js` watches `layout-shift` (input-led shifts skipped) and, where the browser supports it, `long-animation-frame`; a shift session over 0.1 or a frame of 200 ms or more goes to `POST /api/paint` in a throttled beacon, at most one every ten seconds. The server writes one `weave: paint {json}` line per report to its log: the score and px move, the selector of the largest moved element, the route with workspace names and ids stripped, viewport, engine family and build. No field values, names or emails reach it. The endpoint takes 8 KB at most, six reports a minute from one address and 600 a minute in all, and answers 404 on a local install (no `WEAVE_ORIGIN`). The privacy page names the report ([[Issue#1553]]).
- The Terms of Service carry a Billing and Seats section between Account Terms and Cancellation and Termination. It states the free trial length weave runs (`TRIAL_DAYS`), that Editors and Architects take paid seats and Observers stay free, what happens at the end of the trial, how an Architect cancels, and the refund position Clerk Billing gives. The price points at the Billing page and checkout, so the terms print no number that can drift from the Clerk plan. The terms effective date moves to 2026-11-10 ([[Issue#1299]]).
## v0.4.131 (2026-11-10)
- A create tray that stays open takes the next record while the previous write is still in flight. The pending-submit hold covers the dialogs that close on success; a create-and-stay tray hands its draft back as soon as its caller resets it, and the phone create sheet keeps its own guard: the name clears and Create goes quiet until the next name is typed ([[Issue#1445]]).
- A credential field's kind, keystore and parts can be changed after it is created. `updateField` merges them through the key field's own validation, a schema document that edits them applies, and the field dialog's Holds and Kept in pickers now send what they show instead of dropping it ([[Issue#1500]]).
- A create dialog now refuses its own second submit while the first write is in flight, so a double tap, a repeated Enter or two `requestSubmit()` calls make one record instead of two. The guard sits in the shared modal and tray shells, and a refused write re-enables the button so the create can be retried ([[Issue#1445]]).
- A date field with no grain refuses a bare run of digits such as `12345`, which V8's `Date.parse` reads as the year 12345. The check for a four-digit year is anchored, so `12345` and `20260821` are refused while `Aug 31, 2026` is still kept as given ([[Issue#1529]]).
## v0.4.130 (2026-11-10)
- On a phone, **Report a problem** now sits in a record's ⋮ menu, and the bug glyph sits in the Activity sheet's head, so a report can start from the screen that shows the problem (Issue #1023).
- The **+ New** button on a record's related block now opens the create tray with an empty Name and a **Create & link** action, so no record is written before you name it (Issue #1474).
## v0.4.129 (2026-11-09)
- The agent connection walkthrough opens the native workspace `/docs` setup page in a browser and reserves `/mcp` for the client connection (Issue #1528).
- Architecture maps pin the shared main ancestor and reject branch-only revisions before rendering, so squash merges preserve their code citations (Issue #1507).
- **Backups under the keychain store carry key names, never values** (Feature #213): when credentials live in the macOS keychain, `weave backup` writes a `keys.json` entry listing the key names and no `keystore.json`, and the summary line says so. `weave restore` reads that list and names every credential whose value this machine's key store does not hold, so each can be set again with `weave key set`. Under the file store a backup still carries `keystore.json` as before. With no `keystore.key` on disk, an encrypted or `weave://` backup now needs `WEAVE_BACKUP_PASSPHRASE` or `--passphrase-env`. `WEAVE_KEYSTORE=keychain` no longer reads as a key file path. Gate: `test/backup-keychain.test.mjs` greps the archive for a known test value and finds nothing.
- Breadcrumbs and the toolbar stay above a record's banner image, and the banner sits directly below them, on records, tables and spaces (Issue #1538).
- A table in a build spec takes `automations`: rules in the Workflows Script shape (`{name, trigger, actions}`, fields and states written by name, `on: false` to build one Off). `weave_build`, `POST /api/build` and `weave build` create each rule after the table's records, a dry run reports a rule that would read Setup incomplete at its path with the reason, and a re-run keeps a rule of the same name. The Work starter now ships a Done on date and one rule, built On, that stamps it when a task reaches Done (Feature #359).
- CI browser jobs install Playwright's system packages from a cache of `.deb` files keyed on the runner image and the pinned Playwright version, so a slow Ubuntu mirror no longer times out the browser install step. The mirror is reached only when the cache is empty or incomplete, and a missing browser or package still fails the step (Issue #1438).
- CLAUDE.md now says production follows the production branch through the promote workflow and that releases do not move it (Issue #1536).
- A share that reaches a slide but not its deck now renders that slide's `deck.html` without the deck's chrome, style, format or space; a slide in several decks takes its frame from the first deck the share reaches (Issue #1453).
- **A failed webhook or http-request is sent again** (Feature #311): a network error, a timeout, or an answer of 408, 429 or 5xx is retried after 1, 2, 4, 8, 16, 32 and 60 minutes, eight attempts in all. While a delivery waits the Workflows record reads Warning, and Health Reason names the attempt and the next try; a retry that lands returns it to Healthy, and after the eighth attempt it reads Failed. The steps after a webhook now wait for its answer, as they already did after an http-request. The waiting delivery is stored in the workspace, so a restart loses nothing, and the minute schedule tick sends it. A retried webhook keeps its X-Weave-Delivery id. Gate: `test/delivery-retry.test.mjs`.
- A document save names the text it started from as `base` (`PUT /api/entities/:id/doc`, SHA-256 of the stored text), and the answer names the next base. A save from a text another tab or client already changed is refused with 412 and the stored record, so two tabs on one document no longer overwrite each other silently; the refused tab keeps its draft and offers Use mine. A refused document save (409, 412, 413) now stays queued and reads not saved, and Try again on the save mark sends it (Issue #1443).
- **Failed Deliveries, replayable** (Feature #311): a webhook or http-request that runs out of attempts becomes a record of the new built-in `System/Failed Deliveries` table, holding the rule, the record that fired it, the request with any credential held by name only, each attempt and the last error. Setting its Status to Replay, `weave automation replay <record>`, `POST /api/automations/deliveries/<record>/replay` or `weave_automations` with `action: replay` sends it again and runs the steps after it; the record then reads Delivered and the rule Healthy. Gate: `test/delivery-dead-letter.test.mjs`.
- A test pins that a file id from a sibling workspace answers 404 and that an observer's PUT to an HTML file's storage answers 403 (Issue #1434).
- `weave help` and `weave <command> --help` build the registry commands from the command registry: each entry lists its command, positionals and flags with the first sentence of its tool description, so a new registry entry shows in help without an edit. Serve, service, backup, restore, forms and `doc export` stay hand-written beside them (Feature #179).
- A date field with the full grain refuses a value with no four-digit year or a partial ISO date, such as `12`, `Aug 21` or `2026-08`, instead of storing the string as given; changing a text field to a date clears those cells (Issue #1499).
- The agent-surface test names `auth.cookie` (OIDC start and callback, logout, `GET /api/auth/me`, `DELETE /api/auth/sessions/:ref`) as the one hand-written surface and states why: cookie and redirect plumbing that answers no single JSON result. A guard test keeps the list to that one row and no door keeps its own `pick` reader (Feature #179).
- The import scaling check reads the fastest of three interleaved imports at each size, so one stall on a loaded CI runner no longer fails it while a superlinear import still does (Issue #1094).
- **Origin tags on automation writes, and the engine halves of inbound idempotency and the echo guard** (Feature #311): every activity entry a rule writes carries `origin: {workflow}`. Two engine functions wait for the inbound webhook endpoint (Feature #227): `claimInbound(source, key)` acknowledges a delivery key it saw within 24 hours instead of running it again, kept in the workspace so a restart still knows it, and `isEcho(source, change)` answers whether an incoming change repeats values weave sent to that source in the last 10 minutes. Gate: `test/inbound-guards.test.mjs`.
- Replacing an HTML introduction keeps the previous file accessible in Files without rendering its body a second time. Tables with introduction fields show loose HTML as file chips; other tables keep their existing inline previews (Issue #1522).
- Single-file attachment fields can use `preview: "intro"` to show a sandboxed HTML introduction without the file bar or a repeated field heading. Source download, replacement and field settings sit in Introduction options. The existing table body order places it before fields (Issue #1514).
- Tables and spaces can display covers and introductions from their System registry records. Covers remain native elements outside the HTML. Existing record layouts stay unchanged unless a table opts into introduction presentation (Feature #287).
- SVG covers retain their original masks and full artwork. Their explicit image view permits styles while keeping scripts, external resources and same-origin access blocked; the raw file still downloads (Issue #1515).
- Tables and spaces without an icon of their own show the default table or folder icon in the sidebar and the phone drawer, so every row lines up with the breadcrumb and with rows that set an icon (Issue #1394).
- Picker toggle tests wait for queued saves to finish before checking stored dates and tags, preserving the existing persistence assertions (Issue #1516).
- **Automations fire when a polled source changes** (Feature #312): a `listener-changed` trigger on a Workflows record reads a list endpoint through a System/Integrations key on the minute tick, compares the answer with the stored snapshot and runs the rule once per new or changed item, with the item as `trigger.item`. The first poll is the baseline and fires nothing. A cursor and an ETag go back to the source, a change weave itself just sent is an echo and does not fire, an unchanged source is polled less often down to the Script's `maxInterval`, the new integration column Poll Budget caps the polls an hour across rules, and a 429 waits for its Retry-After with the rule in Warning. Health Reason shows the last poll, the next poll and the items seen. The snapshot and cursor survive a restart, and the key is never stored with them. Gate: `test/automation-listener.test.mjs`.
- Production promotes as soon as a main commit's `tests` and `dev-soak` runs both succeed, keeps the hourly schedule as recovery, and alerts when a promotion lags its commit by more than one batch (Issues #1535, #965).
- Public documentation gains native full-logo banners, introductions above fields, and source-managed guides for first steps, linked project work, agent connection, and reading releases (Feature #360). The pilot uses existing Showcase examples and native schema-only templates. v0.4.128 gains an introduction linked to maintained guides while retaining its original release notes. Presentation sync replaces only its own attachments and descriptions, preserves independent edits, and leaves Issue, Feature, Suite and Case schemas and records unchanged.
- A new record takes its public id inside the write transaction from the stored rows, so an automation that runs on create quotes the id the record keeps, even when another writer added a record first. Opening a workspace that holds duplicate public ids still renumbers the later duplicate, and now writes a `public-id-renumbered` audit line naming the table, the record and its old and new ids (Issue #1455).
- The 50,000-row lookup-sort query check compares the sorted first page with the unsorted one measured in the same run, so a loaded CI runner no longer fails it while a sort that walks every row again still does (Issue #1511).
- Accounts, identity links, invites and the workspace subcommands left in the CLI run from the command registry: `weave account`, `weave invite`, `weave workspace require-auth|link-preview|leave`, the `weave_accounts` actions and `/api/accounts`, `/api/accounts/:ref/identities` and `/api/invites` dispatch from one entry each with the same answers. A registry command may have no HTTP door when its route lives elsewhere (Feature #179).
- Record create, read, update and delete run from the command registry on every door: `weave create|get|update|delete`, their MCP tools and `POST /api/tables/<table>/entities`, `GET|PATCH|DELETE /api/entities/<id>` dispatch from one entry each. HTTP keeps the caller's zone, `If-Match` and its 412 with the record, `affected` on PATCH and `ok` on DELETE; MCP keeps its values check and the Users-record refusal; every create door still hands back the one-time invite (Feature #179).
- Documents, their revisions and comments run from the command registry on every door: `weave doc get|set|append`, `weave doc-revisions`, `weave doc-restore`, `weave comment [delete]`, `weave_get_doc`, `weave_set_doc`, `weave_doc_revisions`, `weave_doc_restore`, `weave_add_comment`, `weave_delete_comment` and `/api/entities/<id>/doc`, `/doc/revisions[/<seq>[/restore]]`, `/comments[/<id>]` dispatch from one entry each. Each door keeps its own answer: `GET .../doc` still answers `{field, doc}`, `weave_set_doc` still answers its length, a comment from an observer still carries the caller's name (Feature #179).
- File attach, read and delete run from the command registry on every door: `weave file [attach|read|delete]`, `weave_attach_file`, `weave_files` and `/api/entities/<id>[/fields/<field>]/files`, `/api/files/<id>` dispatch from one entry each, with the same answers, ETag and 304 behavior as before (Feature #179).
- JSON export and import run from the command registry on every door: `weave export [--out]`, `weave import --file`, `weave_export_json`, `weave_import_json`, `GET /api/export` and `POST /api/import` dispatch from one entry each. The CLI and HTTP export still carry attachment bytes and MCP still leaves them out unless `blobs` is true; only an architect still replaces the workspace over HTTP, and the role gate reads the import as a schema write from the registry (Feature #179).
- Query, trash, stats and bulk run from the command registry on every door: `weave query|trash|stats|bulk`, `weave_query`, `weave_trash`, `weave_stats`, `weave_bulk`, `POST /api/tables/<table>/query`, `GET /api/trash`, `GET /api/tables/<table>/trash`, `GET /api/tables/<table>/stats` and `POST /api/bulk` dispatch from one entry each. Each door keeps its own answer: the CLI query reads relations in full, MCP and HTTP as chips; `GET /api/trash` still carries views and purge; a `where` that is not JSON is still a 400 on stats; an editor is still refused a bulk write to a system table (Feature #179).
- Link, unlink, workflow state and restore run from the command registry on every door: `weave link`, `weave unlink`, `weave state`, `weave restore`, their MCP tools and the `/api/entities/<id>/link|unlink|state|restore` routes dispatch from one entry each, and the HTTP answers still read the record in the caller's zone (Feature #179).
- The schema and build commands run from the command registry: `weave schema`, `weave schema export`, `weave schema apply` and `weave build`, the tools `weave_schema`, `weave_apply_schema` and `weave_build`, and `GET` and `PUT /api/schema` and `POST /api/build` dispatch from one entry each with the same answers, and an editor is refused the writes by the registry's `writes: schema` (Feature #179).
- Search and undo run from the command registry on every door: `weave search`, `weave undo [list]`, `weave_search`, `weave_undo`, `GET /api/search`, `GET /api/undo` and `POST /api/undo` dispatch from one entry each. Each door keeps its own answer: the CLI search still runs the record search, MCP and HTTP the universal one; `weave_undo {list}` still answers `{history}`, the CLI and HTTP the bare list. `weave undo list` is new and lists like `weave undo --list` (Feature #179).
- `GET /api/share`, the share token's own read, dispatches from the command registry with the same answer and the same 404 without a token (Feature #179).
- The share family runs from the command registry on every door: `weave share [list|mint|renew|revoke|update|regenerate]`, `weave_shares` and `/api/shares[/<id>[/renew|/regenerate]]` dispatch from one entry each. HTTP still adds each grant's link, `weave_shares list` still answers `{shares}`, and only an architect still manages shares. `GET /api/share`, the token's own read, stays hand-written (Feature #179).
- Space list and space members run from the command registry: `weave space [list|members]`, `GET /api/spaces` and `/api/spaces/<space>/members` (read, set, remove) dispatch from registry entries with the same answers as before (Feature #179).
- Table list and table views run from the command registry: `weave table [list|view]`, `weave_table_view`, `GET /api/tables` and `/api/tables/<table>/views[/<view>]` (read, write, delete) dispatch from registry entries with the same answers as before (Feature #179).
- Template list and template use run from the command registry: `weave template [list|use]`, `weave_template_list`, `weave_template_use`, `GET /api/templates` and `POST /api/spaces/<space>/use` dispatch from registry entries with the same answers and refusals as before (Feature #179).
- The saved view family runs from the command registry on every door: `weave view [list|get|create|delete|restore|share|unshare]`, `weave_views` and `/api/views[/<id>[/restore|/share]]` dispatch from one entry each. `weave_views list` still answers `{views}`, the CLI and HTTP the bare list; sharing a view still takes an architect on MCP and HTTP. The CLI's unknown-subcommand hint now lists the subcommands in the MCP order (Feature #179).
- The vocabulary lookup runs from the command registry: `weave vocabulary`, `weave_vocabulary` and `GET /api/vocabulary` dispatch from one entry with the same answers and the same refusal of an unknown section (Feature #179).
- The workspace record, logo and rail order run from the command registry: `weave workspace get|set|logo|order|move`, the `weave_workspace` actions and `GET` and `PATCH /api/workspace`, `PUT` and `POST /api/workspace/logo`, `GET /api/workspace/logo`, `DELETE /api/workspace/logo`, `GET /api/workspaces` and `PATCH /api/workspaces/:ref` dispatch from one entry each with the same answers (Feature #179).
- The editor and observer gate reads each route's `writes` from the command registry for every route the registry serves: the `/api/spaces`, table view, tables query and spaces use patterns are gone. A read-only registry command (the formula check) is now open to an observer, as its `writes: none` says (Feature #179).
- **A save glyph at the right of the header** (Feature #217): while a record edit waits in the save queue the header shows a clock, then a tick for two seconds once every edit has landed, and a red alert when a write failed twice and is kept. A tap on it, or Escape, sends every waiting edit at once. It is hidden when nothing waits, names itself ("Saving 2 changes. Save now", "1 change not saved. Try again") and is a 44 px target on a phone. Gate: `test/save-glyph-browser.test.mjs`.
- **Grid cells and record fields save through the save queue** (Feature #217): every edit made in a table cell or a field on the record page or dock goes into the save queue instead of its own PATCH. Four quick edits show one clock in the header and no toast, land as four PATCHes in the order they were made, then the tick. With the server unreachable the edit is retried once, the cell keeps the typed text, the error toast still says "was not saved", the glyph turns red, and a tap on it once the server answers lands the edit. The page going hidden with a field still focused, as iOS leaves it, commits that field and sends it within a second. Cmd-Z and Undo send any waiting edit before they undo, so they undo the edit just made, as before; the activity log and the "changed first" conflict toast read as before. Gate: `test/save-queue-cells-browser.test.mjs`.
- **Documents save through the save queue** (Feature #217): a document edit goes into the same save queue as cell and field edits, with the same 600 ms wait, so the header glyph shows a document save as it shows any other. The small mark beside each document (·, ✓, !) is gone; a document write that fails twice keeps the draft, shows "Your draft is kept and saves on your next edit or when you leave", turns the header glyph red, and sends again on a tap, on the next edit or when the page goes hidden or away. Gates: `test/doc-save-failure-browser.test.mjs`, `test/doc-autosave-phone-browser.test.mjs`.
- **http-request takes signWith, and every credentialed call carries a timestamp** (Feature #311): an `http-request` action that names a keystore credential in `signWith` is signed with the same `X-Weave-Signature`, `X-Weave-Timestamp` and `X-Weave-Delivery` headers as a webhook; one that goes through an integration carries `X-Weave-Timestamp` and `X-Weave-Delivery` beside its key. The Automations guide's new "Signed deliveries" section gives receivers the replay protection: a five-minute timestamp window and the delivery id, which every retry and replay repeats. Gate: `test/delivery-signing.test.mjs`.
- **Starters for agents** (Feature #244): `weave_template_list` now answers `starters` beside the template spaces: every built-in starter's id, title and blurb, and for the four featured starters (Money, Work, People, Notes) a `grows` note on how the starter is meant to grow. `weave_template_list {starter: "<id>"}` answers `{starter, spec}`, a spec dated this month that `weave_build` takes as is, so an agent can pick a starter before it improvises. Gate: `test/starter-core.test.mjs`.
- **Templates gallery** (Feature #244): a Templates link in the sidebar, under + New space, opens `#/templates`, a gallery of every starter grouped by category. An architect picks one to add its space of tables and sample records to the workspace already in use, and lands on its first table. A starter whose space the workspace already holds shows "In this workspace" and opens that space instead of building it twice. Editors and observers see the gallery without the build. The page loads `public/starter-gallery.js` on demand. Gate: `test/starter-gallery-browser.test.mjs` (both themes, 375 px).
- Between 601px and 900px the workspace rail leaves the shell, so the menu button is the only way into navigation and the peek drawer brings the rail back beside it (Issue #1457).
- The drawer that carries the rail between 601px and 900px leaves it live, so its workspace marks still take a click and a Tab reaches them (Issue #1546).
- The record title and view title spellcheck only while focused, so a read-mode title no longer shows a red underline on a phone (Issue #1405).
- The phone touch gate walks the menu again: with every table and space drawing an icon, the space row clears the 44px minimum, so the hold for Issue #1163 and its two `test/touch-held.json` entries leave the ledger (Issue #1540).
- The phone touch gate's offender ledger `test/touch-held.json` now names the platform last in its key wherever the hold in `test/lib/states.mjs` names one, so hit-area debt that only one platform's font metrics show can be held without reddening the other runner, the way `test/cutoff-baseline.json` already holds its counts per platform (Issue #1542).
- A record update or create that is refused partway, such as a relation change into a private space, now leaves every field, linked record, document and activity entry as it was, and its automations and document revisions run only once every value has been written (Issue #1454).
- Automations: a `webhook-inbound` trigger on a Workflows record. Saving the rule mints `POST /w/<workspace>/api/hooks/<id>`; `weave automation secret` shows its secret once (`--rotate` mints a new one). A POST is admitted with an `X-Weave-Signature` over `<timestamp>.<body>` and a timestamp within five minutes, the scheme weave's signed webhooks send, or with the secret as `X-Weave-Secret`. Bodies over 64 KB answer 413, a redelivered key answers 200 and runs nothing, a change weave just wrote to the sender (`from`) is dropped, and an Off rule answers 202. The body is `trigger.body`, each item of `items` is `trigger.item`, and `record` names the record set-field and add-comment write to. Every delivery is logged on the rule with its time, status, size and outcome, and the last one shows in Health Reason (Feature #227).
## v0.4.128 (2026-11-09)
- **Automation pills on the relation map load with the first automation** (Issue #1041): `AUTO_ACTION`, `automationTrigger` and `drawAutomationPills` moved out of `public/app.js` into `public/automation-map.js`, fetched only when the map has an automation to draw. A table, row or home load in a workspace without automations no longer downloads them. A failed fetch shows "Automations did not load" and the map still draws its tables. Gate: `test/automation-map-lazy-browser.test.mjs`.
- A picture dropped on a board card whose table has no cover field now uploads through the prepared raw upload once Add a Photo field creates the field, so the last browser upload path no longer sends the file as base64 inside JSON (Issue #1428).
- Automations run from the command registry on every door: `weave automation`, `weave_create_automation`, `weave_automations` and `/api/automations` dispatch from one entry each, and `GET /api/automations/list?db=<table>` reads the rules as records over HTTP, the way `weave automation list` and `weave_automations {action: "list"}` already did (Feature #179).
- Keys run from the command registry on every door: `weave key`, `weave_keys` and `/api/keys` dispatch from one entry per action, and `weave_keys` still refuses `reveal`. `weave key <unknown>` now lists its subcommands as list, set, reveal, share, unshare, delete. The cross-workspace probe and the route census now cover every registry route (Feature #179).
- **The document editor glue loads with the editor** (Issue #1041): the slash menu, reference hints and chips, toolbar bubble, list, table and Enter keys, uploads, code block toggles and `mountDocEditor` moved out of `public/app.js` into `public/doc-editor.js` (42,283 B raw). `loadVditor` fetches it beside Vditor, so no route parses it before first paint; saves, dash rails, heading folds and teardown stay in `app.js` because a record page paints them at load. A failed fetch falls back to the source box with "The editor did not load". Gate: `test/doc-editor-lazy-browser.test.mjs`.
- **Automation action `http-request`** (Feature #310): a Workflows rule calls an outside API with a method, a url whose path and query take templates, templated headers and a text or JSON body, and maps paths of the JSON answer onto fields of the firing record. `integration` names a System/Integrations record and the request goes through `integrationFetch`, so the key never enters the Script, the activity entry, the step output or the `key-used` audit entry. A url host that is not the integration's Host reads Setup incomplete, and the send checks again. With no integration the request is unauthenticated and passes the webhook address guard. The step output `{status, ok, body}` feeds later steps as `{{steps.N.body.id}}`; a non-2xx answer, a timeout or a refusal stamps Health Failed and stops the run. The automation engine gains `automationsSettled()`, which waits for runs still waiting on an answer. Gate: `test/http-request-action.test.mjs`.
- **`weave key migrate --to keychain` and `--to file`** (Feature #213): moves every credential, its owner and its sharing between `keystore.json` and the macOS keychain, reads each value back from the new store, and only then deletes the old one (`keystore.json` and `keystore.key`, or every `weave:` keychain item). A value that does not read back stops the move with nothing deleted. A move weave would not follow is refused before anything moves: on a Mac the default `~/.weave/keystore.json` follows the move by itself, and any other path needs `WEAVE_KEYSTORE=keychain` or `WEAVE_KEYSTORE=file` set for the move and for every weave that reads the keys. The move is a local operator act: MCP refuses it, and HTTP refuses an account. A running server picks the store on every key read, so it follows a move made from the CLI without a restart. Gate: `test/key-migrate.test.mjs`.
- **Credential values have a key store behind them** (Feature #213): `src/keystore.js` holds two stores behind the engine's key API. `file` is today's `keystore.json` and `keystore.key` pair, unchanged on disk, and stays the default on Linux, in CI and wherever a key file already exists. `keychain` keeps one macOS keychain item per key, named `weave:<workspace id>:<key>`, plus a `weave:keystore` item holding names, owners and sharing; it is the default on a Mac with no key file. `WEAVE_KEYSTORE=file` or `WEAVE_KEYSTORE=keychain` picks one by hand, `WEAVE_KEYCHAIN` names a keychain file, and `WEAVE_KEYSTORE` set to a path still means the file store at that path. Values reach `security` on stdin through `security -i`, never in a process argument list. Gate: `test/keystore.test.mjs`, one suite run against both stores (the keychain cases run on macOS against a throwaway keychain).
- Bold words in a launcher About description stay inline with the sentence around them instead of each breaking onto its own line, on the workspace and the space About panes (Issue #1440).
- **The ontology lists an automation as a record of System/Workflows** (Feature #249): `weave ontology --depth glossary` and docs/ONTOLOGY.md move Automation out of the machinery and into a new records group, with System/Workflows as its registry. Gate: `test/ontology.test.mjs`.
- **One left edge for every phone field value** (Issue #1381): on the phone record page the Due date sat 7 px right of the chips beside it, because a date field puts its input inside a `.date-cell` wrapper and so missed the inset every other value kind carries. One rule now names each value kind that holds an input, so numbers, dates, plain text and chips all start at the same edge. Every normal field row is also a uniform 56 px with 4 px of block padding inside its borders, which keeps a 44 px control off the hairline, leaves the row's label a 44 px tap target, and keeps a focused input's ring clear of the divider instead of growing onto it. Gate: `test/phone-matrix-record-browser.test.mjs`.
- A scheduled `promote` run now acts once the last production promotion is a batch old (`PROMOTION_EVERY_HOURS` less half an hour), counted from the newest `production` deployment. It used to skip any run GitHub delivered in an odd UTC hour, and GitHub delivers the hourly schedule late, so production could lag main by half a day (Issue #1493).
- Audit and activity run from the command registry on every door: `weave audit`, `weave activity [id]`, `weave_audit`, `weave_activity`, `GET /api/audit` and `GET /api/activity[/<id>]` dispatch from one entry each, and every door keeps its own default page (Feature #179).
- CSV export and import run from the command registry on every door: `weave csv`, `weave_export_csv`, `weave_import_csv` and the `export.csv` and `import.csv` routes dispatch from one entry each. Export still prints raw CSV on the CLI and answers `text/csv` over HTTP (Feature #179).
- The registry report and rebuild and the relation map run from the command registry on every door: `weave registry [report|rebuild]`, `weave map`, `weave_registry`, `weave_relation_map`, `/api/registry`, `/api/registry/rebuild` and `/api/relation-map.mmd` dispatch from one entry each. The HTTP role gate reads the rebuild as a schema write from the registry instead of a path pattern (Feature #179).
- **The relation map loads where it is drawn** (Issue #1041): `relationMapView`, `mapCard`, `showMap` and `mapAutomations` moved out of `public/app.js` into `public/relation-map.js`, and `public/relmap-layout.js` is no longer a script tag on every page. The workspace home, an opened space overview, the phone space launcher and `#/map` fetch both files on first draw; a table, board or record page fetches neither. Both files are named in a `#lazy-assets` template in `public/index.html`, so `lazyScript` loads them by their versioned names and a warm home load revalidates neither. A failed fetch shows "The relation map did not load" and the rest of the page paints. Gate: `test/relation-map-lazy-browser.test.mjs`.
- **One save queue for record edits** (Feature #217): `public/save-queue.js` holds the queue every editor will push `{ entityId, field, value }` into. It waits 600 ms after the last edit to a field, sends the writes one at a time in the order they were made, retries a lost connection or server error once, keeps an edit that still fails until the next flush, and reports how many edits are pending. The page going hidden or away (`visibilitychange`, `pagehide`, the event iOS Safari fires) sends every waiting edit at once. Gate: `test/save-queue.test.mjs`.
- **A schema document keeps every field setting it describes** (Issues #1494, #1495, #1496, #1497, #1498): `applySchema` now carries a text field's literal setting, a toggle's on and off labels and a credential field's keystore and pair parts, so `applySchema(describeSchema(x))` rebuilds them instead of falling back to the defaults. An edit to a text literal, a toggle label, an avatar shape or a cell colour in a schema document now plans an update and lands; before, it applied as a no-op. Gate: `test/schema-mirror.test.mjs`.
- Settings names each trashed line by what it holds (Space, Table in its space, Record in its table) where it used to show the registry table behind it, and its note lists tables and spaces beside records and share pages. Delete forever on a table or a space purges it, a space with every table in it, after the exact name is typed and the hold runs out (Feature #160).
- **Money's Months sorts by date** (Feature #244): the Months table in the Money starter carries a month-grain Date field and its standard view sorts on it, so October sorts above November instead of below it by name. The sample month is dated to the first of the month it is built in. Gate: `test/starter-core.test.mjs`.
- **Notes starter** (Feature #244): a fourth featured starter, Notes ("Notes filed by topic, newest first"), joins Money, Work and People on onboarding step 3 and the first-run empty state. It builds a Notes table (Topic, Date, sorted newest first) and a Topics table that counts its notes, with three sample notes under two topics. The note text lives in each row's Description document. Gates: `test/starter-core.test.mjs`, `test/onboarding-browser.test.mjs`, `test/first-run-browser.test.mjs`.
- Trash names each line by what it holds: a trashed space reads Space, a trashed table reads Table in its space, and a record reads Record in its table, where the column used to show the registry table behind it (System/Spaces, System/Tables). Restore on a space or table line brings it back to the sidebar in one click, and a table inside a trashed space says to restore the space first. The table and space menus gain Open trash, beside the Trash row in the sidebar (Feature #160).
- **Workflows Health reads as a coloured state with its reason** (Feature #249): the Health cell is the engine's own read-only chip, green Healthy, red Failed, slate No runs, and a Failed row shows its Health Reason on hover, on click and on a tap on a phone, where Health leads the row. Gate: `test/workflows-health-browser.test.mjs`.
- **The Workflows table reads as the automation control panel** (Feature #249): its Standard view shows On, Name, When, Then, Health and Last Run. When names the table and the trigger in words and Then lists the actions in words, both written by the engine, so you can see what a rule does without opening its Script. Gate: `test/workflows-panel.test.mjs`.
- **A Workflows row that is not set up cannot be switched on from the grid** (Feature #249): while a row is Setup incomplete and Off, its On toggle is greyed and names what is missing on hover, click or tap. A row that broke while On keeps a live toggle so you can switch it off. The engine now keeps the setup reason in Health Reason for an Off row too. Gate: `test/workflows-toggle-lock-browser.test.mjs`.
## v0.4.127 (2026-11-09)
- The accessibility gate now runs every axe rule tagged `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` or `best-practice` instead of five named rules, walks the phone width band as well as the desktop one, carries the width band in each baseline key, and proves itself on a planted unlabelled button. The 82 violations the wider scan found are baselined against Issue #1492 (Issue #1449).
- **Streamed uploads for a picture dropped on a card** (Issue #1486): streamed attachment uploads (Issue #1428) landed and were reverted the same night, because `test/board-cover-drop-browser.test.mjs` still waited for the `POST /entities/<id>/fields/Photo/files` that a board drop no longer sends. A picture dropped on a card reaches the same upload control as the field editor, so it streams through `POST /api/uploads` and `PUT /api/uploads` like every other browser upload, and the three sibling drop suites were moved to that pair while this one was missed. The case now waits for the prepare that names the cover field and for the body PUT that answers 201, and still asserts that the file lands on the row and the tile repaints with it. Gate: `test/board-cover-drop-browser.test.mjs`.
- The `security scan` check no longer fails the whole repository when a GitHub release download answers 504: the scanner downloads retry across 120 seconds, and a download that still fails is annotated as a network failure rather than as a finding in the code (Issue #1490).
- A sheriff flake line now names the step the shard died in, so a browser install timeout is told apart from a test that crashed its shard, and an install failure never adds a line to a per-file flake row (Issue #1353).
- **Browser attachment uploads stream to disk instead of blocking the server** (Issue #1428): a 9.5 MB attachment sent as base64 inside JSON held the event loop for 181 ms while the server parsed the body, decoded it and wrote the file with a synchronous fsync, stalling a parallel health check by 126 ms. The browser now prepares a bounded upload with `POST /api/uploads` and sends the file as the raw body of `PUT /api/uploads`. The server writes that body chunk by chunk to a staging file in `uploads/` beside the workspace, fsyncs it with the async API and renames it into `files/` before the record commits. A small preview rides along in the grant. An upload whose connection ends early answers `incomplete-upload` and reads in the browser as a lost connection, so a cut upload still offers Retry. The JSON attach routes stay for MCP and API callers, avatar fields keep taking their bytes inline, and the 10 MiB limit now counts the file itself, without the base64 overhead. Gate: `test/upload-stream.test.mjs`.
- A held phone touch-target state now has to miss exactly the hit areas `test/touch-held.json` records for it, so a second undersized control on a held surface fails the gate where a bare offender count passed one failure or a hundred. Each entry is a fingerprint naming the control and the axis that misses, never its pixels, so the gate reads the same on a developer's machine and on the CI runner. A planted undersized control proves it, and a held surface that comes clean still asks for its hold to be dropped (Issue #1450).
## v0.4.126 (2026-11-09)
- **The link palette's + New opens the create tray** (Issue #1473): choosing + New in the link search wrote a record named from whatever you had typed, with no chance to name it properly or fill another field. It now opens the right-hand create tray on the typed name, with a Create & link button; the record is written when you press it. Gate: `test/link-search-browser.test.mjs`.
- **The phone + creates in the tray** (Issue #1471): tapping + on a phone opened a bottom sheet whose table chip opened a popover, so starting a row looked nothing like starting one anywhere else. It now opens the right-hand create tray, named for the table's own row word, carrying the Name field, the table's choice chips and, when no table is in context, the table chooser inside the tray. Create still keeps the tray open on a phone for the next row, and the tray now stays above the on-screen keyboard. Gate: `test/phone-new-row-sheet-browser.test.mjs`.
- **Roll up names its new record in the tray** (Issue #1472): rolling a selection up through a relation opened a small popover over the grid to name the parent. It now opens the right-hand create tray with the same Create & link button, so every create surface looks the same. Gate: `test/row-selection-browser.test.mjs`.
## v0.4.125 (2026-11-08)
- A `now()` or `today()` date default reads the creator's wall clock: a floating field stamps the time in the zone the creating request names in `X-Weave-Zone` (UTC when it names none), and a fixed field stamps the time in its own zone. A row created at 21:41 UTC from New York now stores 16:41 instead of 21:41 (Issue #1480).
## v0.4.124 (2026-11-08)
- A record update that names several fields checks every value before it changes any of them, so a rejected update leaves the record as it was in memory and on disk instead of keeping the fields that came before the bad one (Issue #1454).
- **AVIF photos show on board cards and open in place** (Issue #1212): an AVIF upload was served as a download (`application/octet-stream`) and the board's picture rule skipped it, so a Shopping List card with an `.avif` photo drew the empty cover. The file route now recognises AVIF bytes and serves them inline as `image/avif`, and a board takes an AVIF file as a cover picture. Gates: `test/attachments-preview.test.mjs`, `test/board-layout.test.mjs`.
- **Hiding a field on a board keeps the board** (Issue #1213): unticking a field in the View pane's Fields section redrew a board view as a table until the next reload, because the instant repaint drew the grid. The repaint now draws the board without the field. Gate: `test/board-layout-browser.test.mjs`.
- **A board view loads its code with its rows** (Issue #1041): `renderBoard`, the card compare view and their helpers moved byte for byte out of `public/app.js` into `public/board-view.js` (20,745 B raw, 6,270 B gzip served). A board view fetches the file in parallel with its rows, and a record page fetches it before painting a related board. A cold home, grid table or row load no longer downloads it, and the `/app.js` gzip budget drops from 173,580 to 165,613. A failed fetch paints the route error with "The board did not load". Gate: `test/board-view-lazy-browser.test.mjs`.
- **Finished logo and loader review sheets leave the repo** (Issue #652): the six `brand/docs/round*.html` review pages from August and September and `brand/build-loader-sheet.mjs`, which only wrote the loader sheet, are deleted. The logo and loader both settled; git history keeps every round. 203,771 bytes and 1,511 lines removed.
- **Main runs eight browser shards at once** (Issue #1351): four shards had grown to 15 to 19 minutes each and were killed at the test manager's 20-minute job timeout on six of seven retried main runs in the week to 2026-11-08. The full suite now plans eight browser shards and runs all ten shards in parallel. Gate: `test/ci-test.test.mjs`.
- **Timing budgets move to a trend lane** (Issues #1237, #1265, #1293, #1309): the vitals and feedback suites time pages in milliseconds on shared runners and turned main red on runner noise, so they no longer gate PRs or main. A `budgets` workflow runs them on every main push and alerts after three consecutive breaches. The budget JSON files, their raise-reason checks and the click and render budgets stay on the gate. Gate: `test/ci-test.test.mjs`.
- **The standalone chip and card anatomy export is gone** (Issue #653): `docs/chip-card-anatomy.html` copied the Handbook guide "Chip and card anatomy" into a page nothing linked to, and a drift test turned every chip CSS change red until someone regenerated it. The guide stays the one copy. The chip CSS extractor the two mockup exporters share moves to `scripts/chip-css.mjs`. 35,797 bytes of HTML and 2,312 bytes of export code removed.
- **One command registry for the CLI, MCP and HTTP** (Feature #179): `src/commands.js` holds one entry per operation with its tool definition, CLI command, HTTP route, what it writes and the one function that runs it; `weave`, `tools/list` and the API dispatch from it. The field writes (`add`, `update`, `rollback`, `delete`, `restore`), `relation add` and `formula check` moved first. Every door now answers those the same way: the API's `DELETE /api/tables/:id/fields/:id` returns the trashed field beside `ok: true`, and `weave field update` (and `weave_update_field` with `verbose`) add the `activity` and `lossy` the API already sent. `weave query --offset nope` is refused with exit 40 (Issue #1374). Gate: `test/command-registry.test.mjs`.
- **Calendar cells share one base rule** (Feature #177): the day cell and the month or year cell in the date picker restated the same border, radius, colour, font, hover and selected rules. They now share `.date-day, .date-pick-cell` rules and differ only in height (28 px and 34 px). Nothing changes on screen. Gate: `test/date-cells-css.test.mjs`.
- **The field dialog's Clock options read as the clock alone** (Issue #1477): on a date field that stores a time of day, a format with spaces in it (long, month, ordinal, quarter) leaked part of the date into the Clock control, so the options read `8, 2026 14:30` and `8, 2026 2:30 PM`. Each option is now labelled from the clock reading alone, `14:30` and `2:30 PM`, whatever the date format. Gate: `test/date-clock-labels-browser.test.mjs`.
- A share link's `deck.html` renders only the slides the share reaches. A share that selects a deck but not its slides no longer shows the unselected slides' content; a space or table share that holds the slides still renders all of them (Issue #1453).
- **Document decorations live in one registry** (Feature #177): the toolbar bubble, reference chips, code auto-expand, code source toggle, outline rail and heading folds each kept their own set, and three places listed the sets by hand. Scheduling, editor teardown and dock release now walk one `DOC_DECOR` list and drop each state through `dropDecor(st)`. Closing the dock now also stops the code source toggle of a document shown there, which the hand-written list had missed. Gate: `test/editor-refs.test.mjs`.
- An open reference chip in the document editor takes its own room in the line and pushes the words after it along, where it used to draw over them. It stays open, with focus on its caret, when the editor redraws its chips. In prose its fields and table badge stand apart instead of running together, and a chip wider than its line keeps its badge and caret in view, so it can always be closed (Issue #1415).
- A document save the server refuses keeps the draft. The section shows `!`, the toast says the draft is kept, the editor shows the draft again when you reopen the record, and the next edit, flush or page leave saves it. Saves of one document now reach the server in the order you typed them, so a slow older save can no longer overwrite newer text (Issue #1443).
- No access to a workspace now offers Find your workspaces, offers Start your own workspace whenever open sign-up is on, and keeps the login held, so a person without an invite leaves the sign-in loop (Issue #1331).
- Your access was removed now offers Find your workspaces, which lists the person's other workspaces (Issue #1336).
- An invite opened with a login that is already a member leads with Open workspace (Issue #1337).
- **One error code on every door** (Feature #179): a refused call now names the same `code` over HTTP, MCP and the CLI. MCP tool errors add `structuredContent: {error, code, rpc}` beside the `Error:` text, and the CLI prints `weave: error [<code>]: <message>` and exits with the code's number (44 not found, 45 conflict, 46 forbidden, 40 invalid; anything uncoded still exits 1). `src/errors.js` holds the table and AGENTS.md prints it. Gate: `test/error-contract.test.mjs`.
- Opening a file you have already viewed no longer re-reads and re-hashes it: file links are cached for a year under the file id, and a revalidation answers 304 without loading the file (Issue #1430).
- **Files sit directly under Fields on the record page** (Feature #357): a record's Files card used to render after every document, so on a row with a long Description an attached report sat a long scroll away. The card now follows the Fields block, the Deck or Slide preview follows the card, and the documents come after both, on desktop and phone. A row without loose files draws no Files card, as before, and a table whose body order already places the card keeps that place. Gates: `test/entity-files-order-browser.test.mjs`, `test/ui-contract.test.mjs`.
- **Attachment chip links pass contrast in the light theme** (Feature #357): with the Files card in the first screen, axe measured its chip link at 3.98:1 (`#066fd1` on `#e2e6ec`). Chip links now use the darkened primary in the light theme, about 4.9:1. Gate: `test/a11y-browser.test.mjs`.
- **Four browser suites wait on the state they assert instead of racing it** (Issues #1209, #1135, #1304, #1111): the phone list chips suite waits for fonts and two frames, the point at which the app re-fits chip lines, instead of a fixed 200 ms; the WebKit math showcase ignores the benign "ResizeObserver loop completed" notice, which is not a rejection; the desktop Activity pane is measured at rest at 1440 px, where the row has slack for its 340 px basis; and the forms suite waits for an edit to land before reading it back. Gate: the suites themselves.
- **Small ghost buttons come from one builder** (Feature #177): twelve buttons across `public/app.js`, `public/command-palette.js` and `public/field-dialog.js` spelled out `btn btn-sm btn-ghost-secondary tiny` by hand (job retry and dismiss, the whiteboard button, remove photo, attachment remove and upload, link search, table search clear, the appears-in gear, unlink, copy link, remove automation target). They now build through `ghostBtn(attrs, ...children)`, which takes any extra class. Markup and behaviour are unchanged. Gate: `test/ghost-btn.test.mjs`.
- **The Handbook pages are markdown files** (Issue #655): the 55 field, guide and formatting-sample pages that sat in template literals in `src/handbook.js` now live as `src/handbook/fields/*.md`, `src/handbook/guides/*.md` and `src/handbook/samples/*.md`, read when the module loads. Every page is byte-for-byte the text it was, so `handbookHash` is unchanged and no workspace re-syncs its Guide or Fields table. The three counts the MCP guide quotes (core tools, all tools, field types) are filled in from the code at load. `src/handbook.js` drops from 3,367 lines to 439.
- **Links in a previewed HTML file open in a new tab** (Issue #1475): a plain link inside an attached HTML file navigated the preview frame itself, the target site refused to be framed, and the panel went white. The `?view` route now puts `<base target="_blank">` after the doctype, so every link opens a new tab and the panel keeps the file. Links to a spot on the same page (`#section`, `#`) and `javascript:` links still run in place, and a page that sets its own `<base>` keeps it. Gates: `test/attachments-preview.test.mjs`, `test/html-preview-links-browser.test.mjs`.
- **The unsent invite-accepted email is deleted** (Issue #656): `inviteAcceptedEmail` in `src/mail.js` was built and previewable at `GET /api/mail/preview/accepted` but no code path ever sent it. Sending it needs the inviter's address, which an invite does not keep (it records the inviter's account name), so the email, its preview and its tests go. The invite and trial emails and their previews are unchanged; `/api/mail/preview/accepted` now answers 404.
- **The Invite dialog loads when opened** (Issue #1041): `inviteDialog` and `seatNote` moved byte for byte out of `public/app.js` into `public/invite-dialog.js`, fetched on the first press of Invite. A cold home, table or row load no longer downloads them (4,943 B raw). A failed fetch shows "Invite did not load". Gates: `test/settings-view-lazy-browser.test.mjs`, `test/invites-browser.test.mjs`.
- The workspace MCP door page and the Connect an agent guide link Sign in, and Create an account when open sign-up is on, so a browser that lands there without an account has a way in (Issue #1338).
- Left nav: drag a space to reorder the spaces, and a table to reorder it inside its space; the space travels with its tables. A table never changes its space by drag, since that changes who can see it: Move to space… in its ⋮ menu stays the one way (Feature #350, Feature #284).
- **A bad API offset is refused by name** (Issue #1374): `?offset=nope` or `?offset=-1` on `GET /api/audit`, `GET /api/tables/:id/entities` and the third paged route answered a silently empty page; each now answers 400 `offset must be a whole number of rows, not 'nope'`. A missing offset still starts at the first row. Gate: `test/route-limit-number.test.mjs`.
- **weave's own model is served** (Issue #654): `weave_ontology {depth: "glossary"}`, `weave ontology --depth glossary` and `GET /api/ontology?depth=glossary` answer the `ONTOLOGY` record (the entity, its levels, what each is made of and the other names for them) that only the tests read before. `weave_ontology` is now one registry entry for all three doors. Gate: `test/command-registry.test.mjs`.
- **The PDF font moves out of the web root** (Issue #659): `DejaVuSans.ttf` (757,076 bytes) and its licence move from `public/vendor/fonts/` to `src/vendor/fonts/`, next to `src/pdf.js`, its only reader. No page loads it, and `/vendor/fonts/DejaVuSans.ttf` is no longer a static URL. The in-tree PDF writer stays: at 16,997 bytes it is a 75th the size of pdf-lib with fontkit (1,283,539 bytes), and every library measured still needs the same font.
- A phone card's title line now reads #id, state chip, name, in that order. The field chip line starts under the state chip instead of at an inset of its own, and sits 24px below a one-line title instead of 26px, the closest it can get while the state chip keeps its 44 by 44 tap area. (Issue #1441, Issue #1387)
- **Popovers share one placement rule** (Feature #177): the chip popover, the search picker and the date picker each carried a copy of the drop-below, flip-above math with different edge clamps. They now place through `placeNear(pop, anchor, { gap })`, which keeps every popover 8 px inside the viewport on all four sides. A chip popover opened from a chip near the top of a short window no longer flips above the top edge, and a picker opened from a cell scrolled past the left edge stays on screen. Gate: `test/place-near.test.mjs`.
- **Popovers share one radius and shadow** (Feature #177): the chip popover, the date picker and the bug panel each restated their corner radius (8, 10 and 9 px) and drop shadow. They now read `--wv-pop-radius` and `--wv-pop-shadow` from `:root`, the chip popover's values, so the date picker and bug panel corners drop to 8 px and their shadow matches the chip popover. Gate: `test/pop-tokens.test.mjs`.
- Two writers on one workspace, such as the server and the CLI, no longer give two records the same public id. The id is checked against the stored rows inside the write transaction and moves to the next free number when another writer took it first, and the public id index is now unique. Opening a workspace that already holds duplicates keeps every row: the first holder keeps its number and each later one takes the next free id (Issue #1455).
- **The Quality mirror carries each case's last result and syncs over HTTP** (Issue #1244, Issue #1245): the CI reporter now records a pass or fail per test, and each shard result carries them. `src/quality-mirror.js` plans the sync as a list of writes: a case red on every attempt reads Failing, red then green on a retry reads Flaky, green reads Passing, and a case no run names takes its file's result when the file is green. `scripts/quality-sync.mjs --from <tracker> --token-env <name> --results <dir>` applies that plan to a tracker over HTTP, removes suites whose file is gone, and fails when drift is left after the sync; `--check` reports the drift and writes nothing. Gates: `test/quality-mirror.test.mjs`, `test/quality-sync.test.mjs`.
- **The Quality mirror syncs only from main's newest tested commit** (Issue #1244): `tests` runs finish out of order, so `.github/workflows/quality.yml` now skips a run whose commit main has moved past. A late run for an older commit no longer rolls the mirror back to an older test tree, and runs for commits from before `scripts/quality-sync.mjs` existed skip instead of failing.
- **The tracker's Quality mirror syncs after every main tests run** (Issue #1244, Issue #1245): `.github/workflows/quality.yml` runs when the `tests` workflow finishes on a main push, red or green, checks out the commit that run tested, downloads its shard results and runs `scripts/quality-sync.mjs` against https://weave.grunion.ai/w/weave with `WEAVE_TRACKER_TOKEN`. The job fails when drift is left after the sync. It runs outside the `tests` workflow, so a tracker outage never turns main red or wakes the sheriff.
- **Shift+Enter creates in the tray** (Issue #777): on a view with no inline add row, such as a board, Shift+Enter opened a centered modal titled with the generic row word. It now opens the right-hand create tray, titled with the table's own row name (New task on Tasks). Gate: `test/new-record-focus-browser.test.mjs`.
- **A table with no term of its own says record, not row.** New tables read "+ New record" and "Search records", and every surface that names the thing itself says record. Row stays the word for grid geometry (row density, the Σ row, a CSV header row), and a table can still pick Row as its term. A `weave_build` table takes `records`; `rows` keeps working. This reverses the default set in Issue #618.
- **Reference panels show only when the record eye turns them on** (Issue #1479): every record drew its References and Referenced by cards and opened the side column for them. The record's Show / hide fields popover now has a Reference panels section with a switch for each card, stored on the table as `referencePanels` (PATCH `/api/tables/:id`, describeSchema, export and import, duplicate). Both start off, so a record draws no reference card and keeps its side column closed until a reader turns one on. Gates: `test/backlinks.test.mjs`, `test/table-config-fields.test.mjs`, `test/references-browser.test.mjs`.
- **References counts links into other workspaces** (Issue #1478): a record whose body linked rows in another workspace, such as Net `Backlog/Tasks#166` pointing at ten weave Issues, showed only its local references. `referencesFrom` now returns an external entry (workspace slug, uuid, link text, URL) for each permalink that names another workspace by `/w/<slug>/` or a `<slug>.weave.` host, and the References card shows each as a plain link tagged with its workspace, counted in the header. The other workspace's rows are never read. Gates: `test/backlinks.test.mjs`, `test/references-browser.test.mjs`.
- **The schema gate reads the command registry** (Feature #179): the API refuses an editor token a table or field write because its registry entry says it writes the schema, in place of five path patterns typed beside the routes. The same writes are refused as before. Gate: `test/command-registry.test.mjs`.
- **Space and table writes run from the command registry** (Feature #179): create, update, delete and restore for spaces, and create, update, move, duplicate, delete and restore for tables are one entry each in `src/commands.js` for the CLI, MCP and the API. The doors now answer alike: a space or table delete answers `{ok, space|table, deleted, hard}` everywhere, and a space update answers the space with its members on every door. `weave table move` refuses a move with no destination space the way the API did. Gate: `test/command-registry.test.mjs`.
- Table and Board are now the only layouts. A saved List view opens as a Table with its filters and group levels intact, and its ticked rows return to their groups. Nest by, the `nest` view key and the CLI `--nest` flag are gone, and `layout: "list"` is refused (reverses Feature #276).
- **One tree gives one security scan result** (Issue #1302): the semgrep baseline now allows a fingerprint it lists however many times a run reports it. A taint rule that matched one listed line twice on one run and once on the next turned PR #564 red with no scanned file changed. A fingerprint the baseline does not list still fails the scan. Gate: `test/security/security-scan.test.mjs`.
- **The Trash and Settings pages load when opened** (Issue #1041): `showTrash`, `showSettings`, the workspace delete dialog and their hold helpers moved byte for byte out of `public/app.js` into `public/settings-view.js`, fetched on the first visit to `#/trash`, `#/trash/<table>` or `#/settings`, or the first Delete workspace. A cold home, table or row load no longer downloads them (10,046 B raw, 3,167 B gzip, 2,740 B br). A failed fetch shows "Settings did not load" and leaves the app running. Gate: `test/settings-view-lazy-browser.test.mjs`.
- **The engine reads workflow states, state categories, cell colours and system sort keys from the public cores** (Issue #658): `src/engine.js` kept its own copies of `STATE_CATEGORIES`, `DEFAULT_WORKFLOW_STATES`, `CELL_COLORS` and `SYSTEM_SORT_KEYS`, with source-matching tests holding them in step with `public/field-dialog-core.js` and `public/cell-graphics.js`. It now imports those cores and exports the very same lists, so a change lands in one file. No behaviour changes. Gate: `test/shared-constants.test.mjs`.
- **A main red held only by timed-out shards convicts no commit** (Issue #1326): when the test manager kills a shard before any file reports, the sheriff files an Issue and alerts instead of bisecting on the gate conclusion and reverting whatever commit sits in the range. Gate: `test/sheriff.test.mjs`.
- **A file preview leaves the document the width of the entity column** (Issue #1476): on a row whose document links another row, the entity page keeps a 320 px references column beside the document. Opening a file preview at 1440 px shrank the entity column to 484 px and left the document 122 px, three or four words a line. The references column now drops below the document whenever the entity column is 820 px or narrower, so the document keeps 466 of 484 px with the preview open. Gate: `test/side-panel-doc-width-browser.test.mjs`.
- A signed-out visit to a workspace host, a row link or `/auth` on a host now opens the weave sign-in page at `/start` and returns to the exact row after sign-in (Issue #1342).
- An invite link with no login opens the same page and resumes the invite after sign-in or sign-up.
- `/start` on a workspace host redirects to the apex `/start` instead of answering 404 (Issue #1343).
- A signed-in person arriving through `/start?next=` for a workspace their account cannot open sees a notice naming it, and the login stays held (Issue #1331).
- The workspace sign-in page says Sign in, names no vendor, asks a workspace architect for an invite, and links to sign-up when open sign-up is on (Issue #1335).
- `/start` accepts a `redirect_url` on the identity provider's OAuth path, so Clerk's sign-in and sign-up paths can point at the weave page (Issue #1342).
- `scripts/clerk-methods.mjs` compares the methods Clerk offers at sign-up and sign-in (Issue #1330).
- When `/api/start` fails, the start page shows an alert with Try again and a support link instead of a bare error line (Issue #1334).
- The weave logo during setup reopens the saved draft at `/start?setup=resume`, so the workspace name and choice survive the click (Issue #1340).
- The supervisor reads the free space on the releases volume before it downloads a release. Under `WEAVE_RELEASE_FREE_MB` (default 128) it fetches nothing and tries again on the next check, and `/api/health` reports `supervisor.disk` with `freeMB` and `totalMB` (Issue #880).
- `railway.json` rebuilds the hosted container only when `Dockerfile`, `railway.json` or `src/supervisor.js` changes, so a push to main no longer restarts the one-volume production container (Issue #548).
- **Number cells swap to their input through one helper** (Feature #177): a number cell's graphic (bar, rating, progress) and its formatted value each carried a copy of the click-to-edit, blur-to-restore swap. Both go through `swapToEdit(display, input)` now. Behaviour is unchanged. Gate: `test/swap-to-edit.test.mjs`.
- **The Tabler stylesheet carries only the rules weave uses** (Issue #660): `public/vendor/tabler.min.css` drops from 536,141 to 87,157 bytes (69,037 to 17,294 gzipped) on every page load. `scripts/trim-tabler.mjs` keeps every element rule, at-rule and design token, and every selector whose classes appear in `public/`, `src/` or `bin/` source, including every class under a prefix the code builds at runtime (`btn-${tone}`). `test/tabler-trim.test.mjs` fails when the app names a Tabler class whose rule the trimmed sheet lacks. Computed styles matched the full sheet on every element of the home page, four tables, two space pages, an entity page, 25 opened menus and the bug form, in both themes.
- **One shared server fixture for unit tests** (Issue #666): `withServer(weave, options, body)` in `test/lib/fixtures.mjs` serves a workspace on a free port, hands the body `{ server, port, base, fetch }`, and closes the server and its keep-alive sockets once the body settles. 50 hand-rolled start, try and close blocks across 38 unit test files now use it. No browser suite imports the fixture, so a change to it selects only the smoke set in the PR gate. Gate: `test/server-fixture.test.mjs`.
- **More unit tests use the shared server fixture** (Issue #666): 44 more start, try and close blocks across 40 unit test files, including the one-line `finally { server.close(); }` shape and blocks with setup lines between the boot and the `try`, now run through `withServer` in `test/lib/fixtures.mjs`.
- **Browser cases that read no paint run in one theme** (Issue #667): 48 light and dark loops across 48 browser suites asserted only geometry, counts, classes or text, which a theme cannot change. They now run once, in light: 84 duplicate dark cases gone, and the converted suites took 144 s instead of 165 s on one local run. `test/theme-loop-guard.test.mjs` fails a new theme loop whose body reads no colour, contrast or screenshot; DEVELOPMENT.md "Theme loops" states the rule.
- **Slash menu source greps that a browser suite proves are gone** (Issue #664): GitHub CI has run every browser suite since the browser shards landed, so `test/ui-contract.test.mjs` drops the greps for the slash menu's group titles, glyphs, syntax column, INSERT heading, prefix score and aliases, each proven in `test/slash-commands.test.mjs`. The greps for wiring a browser cannot see cheaply (row escaping, highlight rules, the promotion threshold, cap and dedupe, and the whole dock block) stay.
- **Workspaces older than v0.4.4 are refused with an upgrade path** (Issue #657): opening or importing a workspace saved before v0.4.4 (a v1 file, a file keyed by `databases`, or percent values still stored as whole numbers) now fails with a message naming v0.4.4 and telling you to open it once with weave v0.4.123 first. The one-time migrations behind those shapes (`databases` to `tables`, `entity.doc` to `entity.docs`, the percent divide-by-100 pass and the table `noun` move) are gone from the open path, which ran them on every open, settle and import. Gate: `test/upgrade-floor.test.mjs`.
- **One helper hooks every overlay's window listener** (Issue #663, Feature #177): the modal, tray, page hold, full-screen viewer, document expand, onboarding, bug panel and the grid's Escape and fill-drag listeners each hand-rolled a window listener that removed itself once its overlay left the page. They now go through `whileConnected(node, type, handler)`, which returns its own `off`. Behaviour is unchanged. Gates: `test/while-connected.test.mjs`, `test/while-connected-browser.test.mjs`.
- **The whiteboard draws with the document's mermaid** (Issue #662): opening a diagram as a whiteboard rendered it a second time through cytoscape and a hand-written flowchart parser, so it could only show flowcharts and drew them in another layout. It now renders the same mermaid SVG the document shows, for every diagram type mermaid draws, inside the full-screen viewer with drag to pan, wheel to zoom and double-click to fit. `public/vendor/cytoscape.min.js` (434,037 bytes) and `public/graph-parse.js` are deleted. Nodes no longer drag one by one. Gate: `test/whiteboard-label-breaks-browser.test.mjs`.
- The phone workspace About pane no longer draws the whole-workspace relation map, which rendered 2,042 px wide in a 339 px card. Each space's About pane keeps its own map (Issue #1421).
## v0.4.123 (2026-11-08)
- On a phone the Change photo button beside a record's name is a 44 px tap target, up from 40 px, in line with the rest of the phone UI. The photo itself keeps its 40 px size (Issue #1167).
- On a phone the field editor tray keeps a 44 px tap target on every control: the option and state rows' colour dot, name box and category picker, the "+ Add" button, the tray's Close button, its text and select fields, and the Cancel and Save buttons in its footer. The colour dot still draws at 18 px inside its new target, and the touch gate now walks the tray (Issue #1187).
- weave can now use a **System › Integrations** key without handing it out: the engine's `integrationFetch` sends one https request to the row's Host with the key in the Authorization header. It refuses every other host, port and scheme, a caller-set Authorization, Proxy-Authorization or Cookie header and a row that is Off or has no key, before anything is sent, and never follows a redirect. Each call stamps Last Used and writes a `key-used` audit entry with the integration, host, method, path and status; a key the host echoes back is redacted from the answer. Nothing calls it from outside the engine yet; Feature #310's http-request action will. (Feature #339)
- The Credential cell of a **System › Integrations** row is write-only: it shows dots and the key's last four characters, with no Show or Copy. **Add key**, then **Replace**, opens a password box that sends the key once, tests it against the row's Host and clears itself; a toast says whether it connected. The columns weave keeps (Key Hint, State, Connected As, Health Reason, Last Checked, Last Used) read rather than edit. (Feature #339)
- Every value on a phone list row's chip line now draws at the select chips' size. A plain number or a date rested as an input, which the iOS no-zoom floor (Issue #724) sets to 16px, so it read larger than the 13px chips beside it; on the phone list, where values are edited from a sheet and never in the row, they now rest as text, as a text value has since PR #646. A formula that returns text or a number draws as the slate chip a select value uses instead of as bare text. When a multiselect or relation hides some of its values, its own `+N` folds into the row's single counter, so a line never reads `+1 +2`. (Issue #1372, Issue #1373, Issue #1388)
- The field dialog's Description box grows to fit its placeholder and what you type, so a phone no longer slices the placeholder's last line, and the placeholder is shorter (Issue #1397).
- A field dialog note such as "lookup field, the type is fixed" keeps the same gap above the next label as every field does (Issue #1398).
- **System › Integrations** is a new system table, listed in the sidebar's System group between Users and Activity, for the outside tools a workspace holds an API key for. Each row names a Provider (Fibery or Generic API key) and the one Host its key may reach. The key goes in once through `PUT /api/integrations/<id>/credential` and is tested against that host on save: a Fibery key must return the current user, whose name lands in Connected As. A green test seals it in the keystore and keeps only its last four characters as Key Hint; a red test stores nothing and says why in Health Reason. Replacing a key keeps the old one until the new one tests green, changing Host or Provider deletes it, and deleting the row deletes it. No route, MCP tool, export, share link, Activity entry or audit entry reads it back: the keystore API refuses every `integration:` name. (Feature #339)
- One rule per text role across every surface and density (Issue #1306): titles balance, prose wraps evenly and folds on a whole line, and `DESIGN.md` and the Handbook guide "How text behaves" write the rule down.
- A description with a paragraph gap inside its fold no longer loses the bottom of its last line above Show more, and a field note folds on its third whole line without a fade (Issue #1306).
- The related subtable on a record page fits its card: a 1% column width no longer inflated it to 19,931 px (Issue #1260).
- A long text value on a phone table row at Spacious ends on one whole line in its pill, where its second and third lines were sliced by the 22 px box (Issue #1420).
- On a phone record page the URL field's link is a 44 px tall tap target and its edit pen a 44 px box, in line with the rest of the phone UI. The pen's icon keeps its 14 px size (Issue #1168).
## v0.4.122 (2026-11-08)
- `DELETE /api/entities/:entity/files/:file` and `DELETE /api/automations/:id` now answer 404 `not-found` when they remove nothing, and 200 with the engine's own body (`deleted: true`, plus `entity` for a file) when they do. Both routes used to drop the engine result and answer 200 `{"ok":true}` either way, so a typo in a file id under a real record read as a successful delete. The CLI and the MCP tools already returned the `deleted` flag and are unchanged. (Issue #1087)
- An attachment's bytes now reach the disk before its record commits. `writeFileDurable` in `src/store.js` writes a `<path>.<uuid>.tmp` with `{ flush: true }` and renames it into place, and the engine's blob writer, the workspace-detach blob path and `restore` all go through it. The store runs `PRAGMA synchronous = FULL`, so until now a power loss after `attachFile`, a preview write, a logo upload, an `importJSON` or a restore could leave a row pointing at a missing or truncated `files/<id>`. A backup also ignores a `.tmp` left in `files/` by an interrupted write instead of archiving it and counting it an orphan. (Issue #1411)
- The four workflow downloads that fetch a pinned release asset, the `gitleaks`, `actionlint` and `hadolint` installs in the required `security scan` and the `mcp-publisher` install in the release job, now carry `--retry 3 --retry-all-errors --retry-delay 2 --max-time 120`. One transient 504 from GitHub's release-asset CDN used to fail the whole check and block auto-merge on every open pull request. The pinned versions and the `sha256sum -c -` verification are unchanged, so a retry that returns corrupt bytes still fails. (Issue #1419)
## v0.4.121 (2026-11-08)
- A backup to `s3://` or `weave://` no longer reads the sealed archive into memory before it uploads: S3 destinations go up as a multipart upload through the existing SigV4 signer, `weave://` streams its PUT body, and the archive is sealed into the file the tar writer is already writing rather than into a second temp copy. Peak RSS for a 320 MB archive drops from 741 MB to 305 MB and stops tracking archive size. `WEAVE_BACKUP_PART_BYTES` moves the 8 MiB part size. (Issue #1410)
- A relation chip's `×` unlinks behind a toast carrying **Undo**, and so does the related grid's unlink button. The phone record page leaves the `×` out of the chip's tap row, where the field's link picker takes a row out instead (Issue #1404).
## v0.4.120 (2026-11-07)
- A checkbox field on a record page answers a tap anywhere in a 46 px box on a touch screen (Issue #1166). The input itself was 16 px, against the 44 px minimum that Apple's Human Interface Guidelines and Material both ask for, and a finger could miss it. The box a reader sees is still 16 px, on the same left edge as the values in the rows around it.
- On a phone, the `›` control on a relation chip in a cell or a field row opens the linked row (Issue #1401). It used to expand the chip in place, which grew the field row to about 290 px of mostly empty box and scrolled the row's name out of sight behind its field values. On a desktop, and in a document on any screen, the control still shows and hides the fields.
- Each icon of a rating field on a record page now sits in a 46 px slot on a touch screen, so a finger can reach it (Issue #1165). Ten icons of a rating with max 10 used to be 14 px each, against the 44 px that Apple's Human Interface Guidelines and Material both ask for. The icons wrap onto a second line in a field row too narrow for all of them, and the glyph is drawn at 20 px.
- A failed upload now says why it failed and what to do next (Issue #1413). A `502` or `503` from the hosting edge, which usually means weave is restarting after a deploy, is retried on its own after about 2, 5 and 10 seconds while the tile reads **Server restarting**; only when the last try fails does the tile turn red with **Still restarting**. A `504` reads the row again and, if the file is not there, says the upload took too long (**Timed out**). A full disk now answers `507` with code `storage-full` on every write, attach routes and record saves alike, and the browser says the server is out of storage with no Retry button. Any other server error shows the request id the server logged, in a monospace face, so a report can be matched to the log. Saves and reads outside uploads use the same causes.
## v0.4.119 (2026-11-07)
- Every MCP tool now carries `idempotentHint` beside its other annotations, so a client knows which calls it may safely retry. Reads, updates, sets, deletes and restores are marked idempotent; creates, appends, row imports, comments, undo, token and share mints, form submits and `weave_call` are not (Issue #1319).
- The New row sheet on a phone follows the keyboard instead of hiding behind it: the sheet sits on top of the software keyboard and caps its height to the page above it, so the Name field you are typing into and the Create button both stay on screen (Issue #1386). The choice-field block takes the squeeze and scrolls, keeping the title, Name and the actions in place. The keyboard's height is published as the `--wv-keyboard` custom property from the visual viewport, so any sheet can read it.
- Uploads show their progress and their failures: each file you pick or drop gets a tile in the field at once with **Uploading 40%**, then **Saving**, and the field is `aria-busy` until it lands (Issue #1361). Every file in a batch is tried on its own and one toast sums the batch up, naming the file that was not saved and why (Issue #1363). A failed file stays as a red tile with its reason and a Retry button, none for a file over the 10 MB limit, which offers **Choose another file** instead. A timeout or `504` reads the row again first, so a file the server stored before the answer was lost counts as uploaded. The document editor's upload button and the avatar picker share the same limit check and messages, the **+ file** picker takes several files (Issue #1365), error toasts carry a red accent and a new amber warning toast marks partial batches, and an error with no JSON body says the server is unavailable or returned an error instead of a bare status number (Issue #1364). The attach routes' `413` body now names its `limit` in bytes (Issue #1360).
- The workspace home opens as a launcher on a phone, the same one a space opens as: a search box over every table, one tile per space with its record count, recent views and records from any space, automation runs across the workspace with any failed step, and About this workspace for the description, the system tables, saved pages and the relation map. Desktop and tablet widths keep the current home, and a workspace with no tables keeps its onboarding card (Feature #355).
- `GET /api/workspace/home` answers the spaces, tables, counts, views and runs the launcher draws, with the same private-space and share-link rules as `GET /api/spaces/:id/home` (Feature #355).
## v0.4.118 (2026-11-07)
- On a table wider than its pane, the add-field "+" now sits after the last column header and scrolls with the grid. It used to hold at the pane's right edge on top of whichever header sat there, so at a tablet width the last visible column lost its label and the grid read as clipped. Scroll the grid to its end to reach the "+"; the Fields menu's New field works from anywhere. (Issue #1356)
- A failed upload stays in the attachments field as a tile naming the reason, with a Retry button that sends the same file again and a Dismiss button that drops it, and the field's file input empties after every attempt so the same file can be picked again (Issue #1362).
- The 10 MiB upload limit is now the size of the file, not the size of the request body: a file that big lands through the browser, the REST attach routes, the CLI and MCP alike, and anything larger is refused by name with the limit in the message (`deck.pdf is 25.0 MB; files can be up to 10.0 MB`), answered as 413 rather than 400, and stopped in the browser before a byte leaves the machine (Issue #1360).
- Removing a file, a logo or a purged row in one workspace no longer deletes bytes that another workspace in the same data directory still names. Every workspace shares one `files/` folder, and importing one workspace's export into another keeps the file ids, so a delete in the copy used to take the original's file with it and the original answered "File blob missing". The bytes now go only when no workspace in the data directory names the file. (Issue #1359)
- A long text or Name cell now expands in place. Rest the pointer on it for 400 ms and the value spills right on its own line from the cell's top-left edge, over the neighbouring columns, and stops at the grid's right edge with a fade where it runs out of room. Clicking it opens the editor in the same spot as a box that runs to the grid edge, then wraps downward and grows as you type; Enter saves and Esc puts the value back, as before. The hover copy used to float above or below the row and the click opened a second, clipped one-line editor under it. Chips, dates and descriptions keep their pop, and touch screens get no hover copy. (Issue #1367)
- The long-text editor that spills right and wraps now opens on the same roomy, ringed box as every other inline editor, 4px outside the value, and a value that fits before the grid edge stays on one line instead of wrapping early. (Issue #1367)
- Collapse all in the View pane folds every group the list or board drew, the option groups with no rows included; it read a model of its own that only knew the groups holding rows, so those stayed open and the button looked dead. (Issue #1203)
- On a phone, a stacked board at Spacious density shows one card per row, so a card's long text reads at full width. Compact and Comfortable keep two cards per row, and cards show their text in full at every density, as before. (Feature #354)
- A Spacious table row shows three whole lines of a long Name, text value or description, set 18px apart in the same 72px row, with the third line ending in an ellipsis. Multiselect and relation chips wrap to two rows of whole chips at Spacious and keep one row at Comfortable and Compact, with a +N counting the rest; at Spacious the hidden "+ link" button no longer takes a row of its own, which had pushed relation chips past the top and bottom of the cell. A plain text value in the table now rests as text, like text with marks already did, so it wraps at Spacious and ends in an ellipsis at the other densities; a click or a typed key opens its input as before (Issue #447). (Feature #354)
- On a phone, a table at Spacious now shows more than Comfortable. Its rows are 118px tall: the name always gets two whole lines, the chips wrap onto two lines of whole chips, and the +N counter sits on the last line. A view with Wrap chips on keeps its two-line name at Spacious too. Comfortable stays at 88px with a two-line name and one chip line, and every row still windows on one declared height. (Feature #354)
- The cut-off gate no longer counts an ellipsis as cut-off text. A label that ends in "…" or a clamped paragraph that ends on a whole line draws every letter it shows, so the gate now fails only on letters sliced by a box edge: a line cut through its middle, a chip cut at its cell's edge, or a clamp whose box is shorter than its lines. The baseline drops its 50 ellipsis and line-clamp instances per platform. (Feature #353)
- An API `limit` that is not a number is refused with a 400 naming the parameter and the value, the way `bin/weave.js` answers `--limit must be a number, not 'lots'` (Issue #1091). `?limit=lots` used to reach the store as `NaN`, and every route that pages answered 200 with an empty page: the audit trail, the undo list, a table's entities, a document's revisions and a `POST` table query all reported zero rows beside a total that counted them. A missing limit, an empty one and a numeric string page exactly as they did. (Issue #1354)
- Importing a workspace into SQLite scales with its row count: 50,000 rows land in 0.9 s where they took 291 s. A full save deleted one row at a time from `entities_fts`, whose `id` column is UNINDEXED, so each delete scanned the whole index and the write path cost grew with the square of the row count. It now clears the index once and inserts every live row, and `Store.save` prepares its four row statements once per save instead of once per row (Issue #1094).
- `weave audit`, `query`, `search`, `undo --list`, `doc-revisions` and `activity` refuse a `--limit` that is not a number with `--limit must be a number, not '<value>'`, in place of SQLite's `datatype mismatch` (Issue #1091).
- The GitHub Pages site unfurls with its own preview image. `docs/_config.yml` carries `docs/social-preview.png` as a front-matter default, the one place jekyll-seo-tag reads a site-wide image from, so every page serves `og:image` and `twitter:image` and the Twitter card is `summary_large_image` rather than `summary`. The repo's own social preview stays a manual upload under GitHub Settings, which no API writes (Issue #1148).
- Phone test cases for Issues #1379 to #1406 run on a five-device iPhone matrix (iPhone SE, iPhone 17, iPhone 17 Pro Max, and iPhone Duo closed and open, modeled from Xcode 27.2) in Chrome and Safari engines. Each case is marked todo until its Issue lands, so it reports without turning the suite red.
- `test/feedback-budget.json` holds `phone-menu|phone` at 250 ms, down from 450. The main CI gate that set the old floor read 217, 259 and 373 ms; with the focus hold off the first frame it reads 147, 124 and 125 (Issue #1061).
- The phone navigation menu paints on the frame after the tap. The focus hold that makes the page behind inert and moves focus into the drawer used to run in the same task as the `nav-peek` class, and its `getClientRects()` reads forced a layout of the freshly revealed full-screen panel before the browser could paint a single frame. It now runs two frames later, so the panel appears first and keeps focus, Escape and Tab a frame afterwards (Issue #1061).
- Editing a table cell or an entity field now opens a roomier box: it grows 4px on every side around the text, rounds to 8px and draws a softer 2px blue ring, in place of the hard 1.5px ring that sat a pixel from the letters. The text, the row height and the column stay where they were. A selected cell that holds chips or a picker draws the same ring. Opening a text field on an entity page no longer nudges the row 3px taller. (Issue #1366)
- Text fields across the app now carry `autocomplete="off"` by default, so Safari no longer offers a Contacts card when you edit a Name cell in a grid (Issue #1375).
- A table view that shows deleted rows now opens on a page and feeds the rest as you scroll, the way the standard view already did. On the weave workspace's 3,980-row Case table the open drops from 5,187,491 bytes to 244,702. Trashed rows keep their strike-through on every page, and the filter readout counts the live rows of the whole table, not of the page in hand. (Issue #1269)
- `weave_query` and `POST /api/tables/<table>/query` answer `liveTotal` beside `total` when a read asks for `includeDeleted`: the live rows among everything the query matched. (Issue #1269)
- A workflow webhook action takes `signWith`, the name of a keystore credential, and signs each delivery: `X-Weave-Signature` carries the hex HMAC-SHA256 of `<X-Weave-Timestamp>.<raw body>`, beside an `X-Weave-Delivery` uuid. A receiver can now tell weave from anyone else without a token hidden in the url. The credential value stays in the keystore, so the Script, the Workflows row, the export and the backup hold the name alone. A rule that names no credential posts exactly what it posted before; a rule whose credential the keystore does not hold sends nothing and reads Failed, since weave never falls back to an unsigned delivery. The verify recipe is in the Automations guide. (Issue #951)
- Opening a large table now has a regression guard: a browser check measures the table-open traffic and fails if the grid ever again asks for the whole table, asks for full related rows instead of chips, or fetches the trash as a second table. The 2,062-row Case table that froze a tab for 17 to 52 seconds opens in under a second on a 3,980-row copy of production, downloading 252 KB instead of 10.2 MB and drawing 75 rows instead of 2,062. (Issue #1357)
- The test runner pins `WEAVE_KEYSTORE` and `WEAVE_DATA` into a per-run temp directory, so a test that skips the override can no longer read or write the developer's own `~/.weave` (Issue #949).
## v0.4.117 (2026-11-05)
- **Automations have a Handbook guide** (Issue #1247): the only prose on automations was one section of *Making a workspace your own*, so the cron schedules (Feature #312) and the `steps.N` outputs (Feature #313) that landed on 2026-10-25 reached no guide. A new **Automations** guide covers the Workflows row as the control panel, all four triggers, the schedule with its cron, zone, `where` clause and catch-up pass, all five actions, placeholders and step outputs with each action's output shape, what a webhook may reach and what marks a rule Failed, a worked example, and the CLI, REST, MCP and grid doors. The customization guide now points at it instead of carrying a second copy. Gate: `test/handbook.test.mjs` (6 cases, the trigger and action sets read out of `src/engine.js` so a new one turns the guide red).
- **Board fold buttons take a thumb on a phone** (Issue #1217): the group and column head folds PR #465 added kept the 22px desktop box on a phone, so the touch gate measured 29x43.5 against its 44px minimum while the lane head, the one head the phone override named, passed. The phone rule now covers every board head, so each fold draws a real 44x44 box the board's horizontal scroller cannot clip the way it clips the `::after` slop. `board-view` and `sink-board` leave their touch hold and are walked by `test/phone-touch-targets-browser.test.mjs` again, which also gains a case per board surface asserting each fold's own box, not just its hit area.
- Clicking an attachments cell in the grid opens its first viewable file in the file panel, the way a chip on the record already does; ⌘-click still opens the file in a browser tab. (Issue #1072)
- ⌘K searches the workspace you are in. The `all` parameter on `GET /api/search` is gone, so the palette no longer merges rows from every workspace a viewer can open, and no row carries a workspace tag; another workspace answers by name as a jump target read from the rail. (Issue #1308)
- `query` takes `shape: 'row'`: a list row of `id`, `publicId`, `name`, `fields` and the `raw` values that differ from their display value, without the `revision`, `url`, `db`, `dbId`, timestamps, actors and documents the full envelope carries. On the weave workspace's 3,980-row Case table, `{fields:['Name','Chip'],relations:'chip'}` answers 2,738,299 bytes instead of 4,380,810. The CLI takes `--shape row` and `weave_query` takes `shape`, 1,642,511 bytes off one list read. The grid, the list and the reference picker still ask for the full envelope (Issue #1270).
- **SOUL.md stops refusing the marketplace weave ships through** (Issue #1145): the README scoped its plugin refusal to "weave has no third-party app store inside it" when PR #472 landed, but `SOUL.md`, which every session writing copy about weave reads first, still quoted the retired line "No marketplace, no extensions, no third-party apps" and attributed it to the README. Its permissions refusal quoted a sentence the README no longer carried either. Both now quote the README that ships. Gate: `test/docs-drift.test.mjs` checks every refusal `SOUL.md` quotes from the README against the README, and refuses a blanket no-marketplace line.
## v0.4.116 (2026-11-05)
- The field-definition clear test times its hold on the page's own clock instead of fixed sleeps: `hold()` from `test/lib/hold.mjs` waits for `.hold-btn.holding` and for the sweep to finish before releasing, the new `releaseEarly()` pauses the sweep midway to prove a let-go hold changes nothing, and the engine is read with `eventually()`. Under load the old 1400 ms hold released before the 900 ms sweep ended and the 400 ms read landed before the write, which failed the gate on change 437 patch set 5 ([[Issue#475]]).
- The full suite balances its four browser shards on measured runtime. The `plan` job downloads the `ci-results` artifacts of the most recent successful main run and weights each file by the `duration_ms` the reporter already records; a file the history does not name keeps its byte-size weight, and a run with no usable history plans by byte size as before. Byte size was anti-correlated with runtime for the worst offenders, since the slowest browser files are small files that loop: on the inventory of run 41959983156 the byte-weighted plan handed the shards 1,171, 1,107, 1,939 and 1,622 seconds of work, and the duration-weighted plan hands them 1,460 each ([[Issue#966]]).
- The full suite's unit shards no longer download Chromium and WebKit. The engine cache and the install step in `.github/workflows/test.yml` carry `if: matrix.lane == 'browser'`, the condition `pr.yml` has had since the PR gate shipped, which gives back the 46 and 37 seconds the two unit jobs of run 39439094335 spent on engines they never opened. A new gate in `test/ci-test.test.mjs` walks every unit-lane test's imports and refuses one that reaches `test/lib/browser.mjs` or imports `playwright` at load, so the lane split stays true to what each shard needs ([[Issue#967]]).
## v0.4.115 (2026-11-04)
- The browser install step of every gating workflow, the PR shards, the main shards and the dev soak, now makes three attempts inside its own budget instead of two capped at five minutes each, waits up to 300 seconds for the dpkg frontend lock, and reports whether `dpkg --configure -a` succeeded rather than discarding it with `|| true`. Three shards died on the runner's apt lock in one afternoon, each costing a full re-run of the job and reaching no test, because an attempt was killed mid-download with the step's budget unspent ([[Issue#1347]]).
- The nightly compatibility matrix names `ubuntu-24.04` instead of `ubuntu-latest`, which GitHub now resolves to `ubuntu-26.04`, and a guard refuses any workflow that carries a floating runner label. PR #507 pinned every other job by name and left this matrix behind, so its WebKit install could stall the way the PR shards did ([[Issue#1258]]).
- A Development/Release row that has lost its notes now stops its own release and nothing else. The export refuses only the row whose version it is writing; an older empty row is named on stderr as historical, keeps the notes `docs/development.json` already carries, and does not block the export, and a boot sync refuses only the row its manifest is for. `scripts/release-commit.mjs` writes the merged commit onto the row by appending one line rather than replacing the document, then reads the row back and refuses when the notes came back empty or without the commit, so a lost document is caught by the run that caused it instead of by the next release eight hours later ([[Issue#1329]]).
## v0.4.114 (2026-11-04)
- The cookie banner says "we record" instead of naming PostHog, keeps every disclosed item, and leaves the privacy policy to name the analytics processor ([[Issue#1344]]).
- A server whose workspaces all detached under an earlier version now starts instead of exiting at `Cannot detach: copied rollup Spaces.Sessions · Cost (USD) · avg is missing`. The hub registry migration required a rollup it still holds to already exist in whichever workspace owns the rolled-up table; a workspace that finished its own migration before the rollup copy shipped never received one, so every start refused and the Mac read copy on `:4400` sat four releases behind from 2026-11-01. The migration now leaves such a rollup on the hub rather than refusing, and still refuses for a workspace it is detaching in that same pass ([[Issue#1307]]).
## v0.4.113 (2026-11-03)
- The vitals, feedback and render budgets are judged on the fastest of their runs rather than the median, so a loaded CI shard no longer turns a budget miss into a budget raise ([[Issue#1327]]).
- Step 0 of the overnight fixer now reads a range of Issue numbers in a merged main subject, so a row landed as part of `Issues #1118, #1143-#1148` stops sitting Open ([[Issue#1313]]).
- The sheriff drops from its suspect list every commit whose diff cannot reach the failing suite, says in the row when nothing in the range can explain the red, and files a repeat failure onto the open row that already names the file ([[Issue#1324]]).
## v0.4.112 (2026-11-02)
- A main red whose only red test files never reached a retry verdict no longer convicts a commit. A shard killed by its own timeout reports each of its files once, so nothing says whether they stayed red; the sheriff now escalates with that file list and logs a line on each file's `flakes on main` row instead of bisecting and opening a revert PR. On 2026-11-02 that path reverted the healthy v0.4.111 release over `test/feedback-budget-browser.test.mjs` and `test/phone-new-row-sheet-browser.test.mjs`, neither of which the run ever judged (Issue #1322).
## v0.4.111 (2026-11-02)
- Billing moves to one per-seat organization plan at $4.99 a seat, from a workspace's first person on: `WEAVE_BILLING_PLAN` replaces `WEAVE_BILLING_SOLO_PLAN` and `WEAVE_BILLING_TEAM_PLAN`, and `POST /api/billing/organization` (**Set up billing** on `/billing`) replaces `POST /api/billing/team`. Neither old variable was set on a deployment, so no workspace changes plan (Feature #256).
- Scroll bars look the same in every pane in Chromium and Safari, whatever the macOS scroll bar setting: one 8px bar with a 4px thumb, in place of a thin overlay on a trackpad and a fat classic bar with a mouse. Bar ends stop short of rounded corners, 16px on the panes, modals and menus and 8px elsewhere. Panes keep the 8px gutter with nothing to scroll, so content does not shift when overflow appears. Firefox keeps its native thin bar and touch screens keep their overlay bars (Issue #1310).
- A space's tables card shows a plain count for a table with no term of its own, and a table's own word where it has one: `331`, `1,139 bugs`.
- A free trial now ends with an email: three days before the end and again on the last day, weave writes to every Architect of the workspace who has a verified address, and each mail links to **Billing**. The sweep runs hourly, marks each stage on the workspace so it sends once, and stops if a plan is bought during the trial. `GET /api/mail/preview/trial` and `GET /api/mail/preview/trial-last` show the copy (Issue #1298).
- The **New user** form now shows the Workspace the account will belong to, filled with the workspace you are in and changeable before you create, and the dialog that shows the one-time token or invite link names the role and that workspace ("Architect token for net"). A saved Users row cannot change its Workspace, and the token is shown once, so the workspace was the one thing you could not check in time (Issue #1224).
## v0.4.110 (2026-11-01)
- The Handbook "Agent access (MCP)" guide follows the MCP primer: read the `weave_ontology` outline, build with one `weave_build` call run with `dryRun` first, read rows with `weave_query` and `weave_search`, and reach every other tool through `weave_call`. Its tool and field-type counts come from the code, icons read `lucide:<name>`, and fields, tables and spaces restore from the trash. The guide now lives in the Handbook source, so a release's Handbook sync replaces the stale row, and a test fails when any Handbook page or doc states a tool or field-type count the code does not have (Issue #1246).
- The overnight fixer's tested core now lives in the weave repo at `agents/fixer/core.mjs`, so a cloud agent that clones only `grunion-ai/weave` can import it. It carries issue selection, batching, claim and attempt notes, enrichment and verdict parsing, plus `eligible` and `changedSinceLastAttempt`, which decide whether a tracker row is open to the fixer. Attempt notes name GitHub PRs, and lines written in the old format still parse (Feature #192).
- `node agents/fixer/verify-dev.mjs <sha>` waits for dev.weave.grunion.ai to serve a merged commit. It polls the anonymous `/api/health` every 10 seconds, accepts a full sha or a prefix of 7 or more characters, refuses a commit whose last swap failed, and exits 1 after `--timeout` seconds (default 900) with the last health body on stderr. The cloud fixer uses it in place of the Mac's `:4400` read copy, which is locked (Feature #192).
- The attachment chip on a board tile, list row or table cell middle-truncates a long single file name so the extension stays visible, and shows the full name on hover (Issue #1155).
- An attachment field's tiles now stay inside their row on the record page. The tile sheet sat in a column flex box whose height Chrome measured before the tiles took their aspect-ratio height, so a tile ran past the row and covered the + file button and the chips under it. The field box is now a one-column grid, which sizes each row to the tiles it holds (Issue #1191).
- **Avatar faces on people surfaces** (Feature #352, relands PR #351): a compact to-many relation cell whose rows all have avatars draws a face pile, up to five faces that each link to their row, then `+N` naming the rest on hover. Created by, Modified by, the activity feed and the identity panel show the actor's System/Users avatar, photo or initials, in place of hashed initials. A one-word name splits on dashes and underscores for its initials, so `weave-fixer` reads WF while `Jean-Luc Picard` keeps JP. Gates: `test/avatar-faces-browser.test.mjs`, `test/chip-renderer.test.mjs`, `test/chip-system.test.mjs`.
- The Board footer no longer shows a stray "null" before the Compare button. The picks slot handed its children straight to the native `replaceChildren`, which stringifies a non-Node argument, so the pick-hint's false branch landed as a text node reading "null" at every pick count under the cap. The slot now filters its children the way `el()` and the members body already filter theirs, and a source check fails when a `replaceChildren` call is handed a child that can be null (Issue #1234).
- Reversed on a board group level now runs the columns, the lanes and the stacked sections backwards, with No value still last. The board built its group levels without the level's direction, so the toggle saved into the view and changed nothing on the page (Issue #1205).
- Board groups collapse into labeled rails or compact stacked headers. Cards follow density, the board loses its extra enclosing border, and comparison checkboxes keep clear of IDs and titles (Issues #1197, #1198, #1199, #1200).
- Drag placement uses settled card positions and waits for the drop animation before refreshing the board (Issue #1115).
- Entity relation blocks show the linked table's default Board layout, with local folds, linked-record editing and New, Link existing and Unlink actions (Issue #1201).
- A board tile sizes itself from its own content and is never stretched or shrunk to its row track, so a layout pass that undersizes the track, as desktop Safari 27 did while a board was still loading, can no longer clip the name down to its descenders. Tiles in a stacked board now take their own height instead of the tallest tile in their row (Issue #1202).
- The brand kit page `brand/kit/index.html`, written by `build-kit.mjs` beside `manifest.json`, shows every kit asset on the ground it is drawn for with its code, size, PNG pixel size, uses and links to the SVG and PNG, and carries the kit README's recipes, palette and rules (Feature #342). Every kit PNG now renders at 2× or more: posters and patterns move from 1× to 2×, lockups stay 3× and the icon 4×, `manifest.json` states each asset's `scale`, and the test reads every PNG header to refuse a raster under it (Issue #1196). The kit gains the 1280 × 640 GitHub social preview `poster-github` (X8).
- **Chips wrap inside board tiles and field rows** (Issues #1124, #1125, #1151, #1152, #1154): a relation, tag, select, state or field-definition chip inside a board tile field or a record field row wraps its label onto further lines instead of cutting it at the tile or row edge, an unbroken word breaks where it has to, and the relation caret stays whole beside the wrapped name. Gate: `test/chips-wrap-browser.test.mjs`.
- Workspace setup gives each named workspace its own connector name and uses it consistently when adding and authenticating the connection (Issue #1276).
- MCP sign-in requests profile and offline access so dynamically registered connectors can sign in and renew tokens with Clerk’s default scopes (Issue #948).
- The breadcrumb keeps the workspace name whole instead of capping it at 14 characters, and a long row name gives way before any short space or table crumb loses a pixel (Issues #1127, #1158). The current crumb's own ellipsis stays by design, since the title row under it shows the full name, and the cut-off probe skips it (Issue #1159).
- A formatted number that runs past its grid column now shows its compact form ($98.8B for $98,765,432,109.87) with the full amount in the cell's title, so a reader never sees a cut prefix of an amount; a number that fits keeps every digit, and widening the column brings the digits back (Issue #1160).
- The cut-off baseline tightens after the board, chip and breadcrumb fixes: on linux the mention chip label row drops from 42 to 41 on desktop and leaves the phone cell, where two CI runs in a row measured none, so the gate now fails on any return of that cut-off (Issues #1152, #1161).
- A new main merge no longer ends or restarts the dev soak, so one fifteen-minute window now reviews a batch of commits. Merges queue behind the running soak and the next window takes the newest commit. A soak whose commit dev moved past during the window keeps its clock and smoke schedule, posts `dev-soak` success naming the commit dev moved to, and stops applying the worker pid check and the requirement that its own watch passes. Each poll checks the release dev serves now for a rollback or a failed watch, and a failure after a move posts pending as superseded with no alert, leaving the queued soak to catch it. Gate: `test/dev-soak.test.mjs`. (Issue #964)
- A dev soak that watched its commit pass and held dev for the full fifteen minutes now posts `dev-soak` success even when dev swaps to a newer main commit on the poll that completes the window, so a repository taking a merge every few minutes still produces commits the promotion workflow can pick. A soak cut short before the window elapses posts `dev-soak` pending described as superseded, where it used to post no status at all and leave promotion reading the workflow's own check-run conclusion instead. Gate: `test/dev-soak.test.mjs`. (Issue #964)
- The dev soak sends its write token only to the write workspace, so a token from another workspace no longer fails the read smoke and blocks promotion (Issue #1292).
- `/docs`, the README, AGENTS.md and the handbook no longer say MCP needs the Architect role. Any role connects, and `tools/list` names only the tools that role may call: an Observer reads and comments, an Editor also writes rows, an Architect also changes the schema and accounts (Issue #1121).
- Done state chips take a darker green label. The old `#218358` on the chip's own `#e2f2e5` tint read 4.05:1, under the 4.5:1 floor, in the field dialog, the filter popover, the board and the table in light theme. The new `#1a6b48` reads 5.57:1, and the dark theme is unchanged (Issue #1100).
- A browser install retry no longer dies on the dpkg lock a previous attempt's holder still owns; it waits up to 180 seconds for it, which fits inside the attempt's own five-minute bound (Issue #1141).
- CI browser jobs now wait for the dpkg frontend lock instead of failing on it: the apt drop-in every gating workflow writes carries `DPkg::Lock::Timeout "180"`, so the runner image's own boot-time `apt-get` no longer costs a shard its install step (Issue #1230).
- `GET /api/entities/:id` and the other row routes take a URL-encoded permalink in the path, so `/api/entities/https%3A%2F%2F…%2Fe%2F<uuid>` names the row instead of answering not found (Issue #1140).
- **The tab icon shows the whole weave mark** (Issue #1203): the favicon frames the rope at `8 8 32 32` instead of the 48-unit mark frame, so it fills 89% of the icon rather than 60%, and `favicon-16/32/48.png` and `favicon.ico` are rebuilt from it.
- A click on a file chip, on the record or on the loose Files card, or on a sheet cell now opens the file in a panel to the right of the record instead of downloading it or jumping to full screen. The panel shows images, PDFs, plain text and HTML (an HTML file loads from its sandboxed `?view` route), steps through the field's files, opens the same file full-screen from its full-screen button, and closes from its ✕ or Esc. ⌘-click still opens a browser tab, and a file with no viewer stays a download link (Issue #1173).
- The record page's Files card, which lists files no attachments field holds, is now a body block: it has a drag grip, a caret that folds it and remembers, and a place in the table's body order (`@files`). **Make a field** in its header adds an attachments field to the table and moves those files into it, so they take the field's preview and multiple settings (Issue #1172).
- Board covers take a Fit setting: Crop (the 4:3 default), Fit, Original or Capped, a Card shape of 4:3, 1:1, 3:4 or 16:9 for Crop and Fit, and Masonry or Rows for the uneven cards of Original and Capped (Issue #1280).
- The README, the GitHub Pages site and llms.txt describe weave as a self-hosted work database for people and AI agents and match what ships: Board layout, the View pane, forms, share links, templates, the phone layout, weave cloud and the published container image are described, and nothing says weave has no hosted instance, no narrower grant than a role, no forms or no marketplace (Issues #1118, #1143, #1144, #1145, #1146). The website's four-layer stack animation opens "What weave is", in light and dark. `scripts/screenshots.mjs` builds a scratch Launch workspace and shoots all seven stills from it, adding a kanban board and a phone still (Issue #1147). The Pages site carries a short title, a tagline and an image, and `docs/social-preview.png`, the GitHub social preview, reads SELF-HOSTED WORK DATABASE (Issue #1148).
- A file tile with no picture (PDF, text, HTML and other files) now shows the file name as it is spelled instead of in capitals, keeps only its kind word (PDF, TEXT) in small capitals, underlines nothing on hover, and carries a 20 px remove button in place of the white box that crowded its corner (Issue #1192).
- Board and list group heads now wrap a long group label onto as many lines as it needs, so a column or group always says which workflow state or option it holds. The label used to end in an ellipsis once it outgrew the column, which cut a 48-character state name by 175 px on desktop and 75 px on the phone (Issue #1157).
- Group headers in the Table and List layouts show the groups above them, muted, before their own field and value ("High › Nav ›"), and the top-level header stays pinned under the column headers while its rows scroll, even far into a long group. With Σ Rollups off, each header also shows a short breakdown of the first single-select or workflow field not grouped at or above it, counting live rows only. Collapsed groups are remembered per grouping: every view of a table with the same group fields, orders and directions opens with the same groups collapsed, and a fold in one reaches the others. A different grouping, including the same fields in another order, starts expanded (Feature #348).
- The View pane's Group section has a **Hide empty groups** switch for List and Board layouts. With it on, groups with no rows drop out of the view and out of `weave_query` groups; with it off, an unfiltered view still shows every option of the group field. The choice saves on the view as `hideEmpty` and travels through `weave_table_view`, the CLI, REST, view copies and `describeSchema` (Feature #345).
- `weave_table_view` names `hideEmpty` in its schema; the tool's byte budget rises from 1,680 to 1,725 to carry the key (Feature #345).
- **Every delete is a hold** (Feature #346, Kyle's ask of 2026-10-28, which supersedes the one-click trash of the 2026-10-10 D6 ruling): Move to trash in the table, record, space, share page and field menus, Move to trash on the selection bar, the Delete workspace dialog from Settings and the workspace menu, Revoke on a share link and on a pending invite, Delete link in the Share panel and Remove logo on the workspace rail each delete only when the press is held until the fill sweeps across. A click lets go early and deletes nothing. Menu rows carry a HOLD hint, and the selection bar tooltip reads Hold to move to trash. The Undo toast stays on every trash. In the Delete workspace dialog the typed name arms the hold, and Enter alone no longer deletes. The Views list delete and Settings Delete forever already held. Hold buttons take no text selection, callout or scroll gesture on touch. A contract test counts every DELETE call site in the app and fails until a new one is a hold.
- An HTML file on a record now renders inline by default, one frame per file in a many-file field too, sized to its own content height: a long page grows the frame instead of scrolling inside it, and a short page shrinks it. The `?view` response adds one inline script after the doctype that posts the page height to the parent; weave sizes a frame only from messages whose source is that frame's own window, and the page keeps its sandbox with no access to weave's origin. The stored file bytes are unchanged (Issue #1175).
- An HTML file that saves its own edits to `localStorage`, such as a deck that autosaves, now keeps them in weave. The `?view` page gets an in-memory `localStorage` and `sessionStorage` seeded from the file's saved snapshot; each `localStorage` change goes to the parent window, which accepts it only from that file's own frame and saves it, debounced, through the new `PUT /api/files/:id/storage` (`GET` reads it). A reload or another device starts from the saved snapshot. The page keeps its sandbox, so it still cannot reach the API or weave's cookies. The file bytes never change; the edits live in the file's page storage, capped at 5 MB per file, and a larger write is refused with a message (Issue #1176).
- An icon name the vendored set does not carry now draws an empty icon box. The lookup went straight into `innerHTML`, so a miss wrote the word `undefined` into the span, and because an icon box is 16px wide with its content centred, that word overflowed about 22px on each side and took taps there. On a phone the Share (Beta) button's miss reached back into Copy permalink's 44px target, 23px to its left (Issue #1235).
- The open-table INP budgets rise to 160 ms on desktop and 112 ms on phone, with the measurements written under `raised` in `test/vitals-budget.json`; the old 80 and 64 ms budgets sat one 8 ms quantum above the green runs and failed on slower shared runners with no code change. A cheaper dock render will try to restore them (Issue #1215).
- A new relation's inverse field takes the source table's name as written, so a table named `Action Items` gives `Action Items` instead of `Action Itemss`. On the many side it uses the plural term set on the source table's Name field, cased like the table name (`Deal` with the term deal/deals gives `Deals`); with no term, or on the one side, it keeps the table name. An explicit inverse name still wins, the field dialog's placeholder previews the same name, and existing fields keep their names (Issue #1262).
- Opening a workspace renames an inverse field that the old rule doubled, such as `Action Itemss` on a table related to `Action Items`, to the name the new rule gives. A rename that would clash with a field already on that table is skipped, each rename is logged in the audit trail, and a second open changes nothing. Names like `Deals` from a `Deal` table are left alone (Issue #1262).
- A long key in a key chip now breaks across lines on board tiles and the record page, so the whole value is readable instead of ending in an ellipsis or being clipped at the tile edge. The grid keeps its one-line chip with the ellipsis and the hover title (Issue #1153).
- Fix: `DELETE /api/keys/:name/share` with `{"account":"bob"}` now revokes that account's share; the body was never read, so the call answered 200 and left the share in place (Issue #1086).
- List: the add-row name field now keeps a 44 px hit area on touch, up from 32 px (Issue #1169).
- The list draws its first 200 rows per group and the Board layout its first 40 cards a column, each ending with a **Show more** button that adds the next batch in place. A 2,000-row list now paints 200 rows and 3,455 elements where it drew all 2,000 and 28,636, and the Board layout 40 cards a column and 2,247 elements where it drew 667 a column and 26,666. What Show more drew survives a redraw, a row added past the cap is drawn, and the button keeps its height in a masonry gallery and a 44 px target on a phone (Issue #1266).
- Every main push now runs its `tests` gate in its own concurrency lane, keyed by the commit it tests. A shared lane holds one pending run at a time, so a newer merge cancelled the one waiting behind it: eight merges in the half hour from 18:32Z on 2026-10-25 left nine consecutive main gates cancelled and the deploy watcher two hours behind the tip (Issue #1181).
- Opening a workspace MCP address such as `https://<slug>.weave.grunion.ai/mcp` in a browser shows a page instead of raw JSON (Issue #1195). It names the workspace, shows the MCP address with a copy button, gives the `claude mcp add --transport http` command and the steps for Claude.ai and ChatGPT, and links the Connect an agent guide at `/docs`. The `401` keeps its `WWW-Authenticate` challenge, and MCP clients, JSON requests and requests with no `Accept` header get the same JSON body as before.
- MCP file uploads can prepare a one-use raw-byte upload grant for a row or attachments field. Local scripts upload directly without sending base64 through the model or creating another editor token. Grants expire after ten minutes and enforce workspace, destination, byte count and current account permissions (Issue #1275).
- **Hiding the sidebar hides the workspace rail too** (Issue #927): on a desktop window the hide button now sits after the workspace name, and hiding takes the rail and the sidebar away together, so the page runs flush to an 8 px gutter. The show button slides in at the head of the page's crumb row and the workspace icon and name move over to make room. Moving onto the left edge still peeks the nav, now with the rail beside it; a pointer parked on the edge no longer pops the peek open when the page scrolls or redraws under it. The floating `#nav-expand` button and the collapsed rail's route tiles (Issue #909) are gone. Gate: `test/nav-toggle-browser.test.mjs`.
- A long table name in the left nav now ends in an ellipsis, with the full name on hover, and the nav no longer scrolls sideways (Issue #1116).
- **Opening a row paints the record at once from the row the grid holds** (Issues #1057, #1059): the dock, and the full-screen record on a phone, open in the first frame with the name, the crumb and every field value the grid already has; the document, the remaining fields, lookups, files and references stream in when the entity request lands, and a value the grid painted is left in place unless the server holds a different one. The fetched entity stays in the warm cache, so reopening the row within five seconds sends no second request, and any write clears that cache. Measured with the API held one second: `open-row|desktop` and `phone-open-row|phone` now answer in the first frame instead of after the server. Gates: `test/dock-first-paint-browser.test.mjs`, `test/feedback-budget-browser.test.mjs`.
- The entity pane keeps a 360px floor beside the View, Share and Activity panes. A window too narrow for every open pane at its floor hides the table panel until a pane closes, and if that is still not enough the View or Share pane floats over the right edge, so the record title no longer stacks one letter per line and the entity toolbar stays clickable (Issue #1286).
- **The parity matrix and the Airtable and Fibery pages list Board, List and Forms as shipped** (Issue #1220): `docs/PARITY.md` scores Board / kanban, List view, Forms and row avatars ✅, which moves parity from 81.5% to 87.1%, and the comparison pages map Airtable's kanban, gallery and forms and Fibery's board, list and forms onto weave's. `test/docs-drift.test.mjs` requires those rows ✅ and refuses the old "removed in Issue #75" wording in the three docs.
- A row, table or space permalink no longer carries `/w/<workspace>`: `url` is `/e/<uuid>`, `/t/<uuid>` or `/s/<uuid>` on every door, and the ⧉ buttons copy the same. A workspace host such as `net.weave.grunion.ai` already names the workspace. Anywhere else, a bare link opens in whichever workspace holds it: the server answers `302` to `/w/<holder>/…`, or `301` to the holder's own host when workspace hosts are on. Renaming a workspace no longer breaks links, and links already shared with `/w/` keep working (Feature #203).
- On a phone a docked record's table name ellipsizes inside its crumb chip, so the #number stays whole and clear of Copy permalink. A long table name used to run under Copy permalink and the header controls (Issue #1301).
- On a phone, the row page lists its fields flush on the page: the outlined, rounded panel around the Fields block is gone, the rows keep their hairline dividers, and each label starts on the same edge as the section headings below it (Issue #1285).
- On a phone, every control in the board and list views now takes a 44 px tap: the board's compare checkbox and short tile titles, and the list view's fold button, row titles, and state, select and multiselect chips. The chips keep their drawn size; each gets an invisible 44 px hit area, and the list row spaces its lines so neighbouring hit areas never overlap (Issues #1128, #1129, #1130, #1131, #1132, #1133, #1134).
- On an iPhone the phone menu scrolls as one column: spaces, + New space, the System group and the identity panel move together, so the space list no longer shrinks to 108 px of the 595 px menu in WebKit with the iPhone 15 profile and the second space stays in view. A browser case fails if the list scroller holds under half of the menu or any space or table link leaves view, and the menu touch hold no longer names this row (Issue #1164).
- Spaces are open or private. Open stays the default, so a one-person workspace sees no change. A private space shows itself to its members and the workspace architects only: anyone else gets a 404 on its tables and rows and finds nothing of it in search, the space and table lists, the schema, the ontology, the System registry rows, views, MCP tools, CSV and markdown exports, share links and the audit log. A relation from an open space into a private one shows a locked chip named "Private", and lookups, rollups and formulas leave the private rows out. A member may carry a role for that space that overrides the workspace role, so an Observer can edit in Sales alone, and a space Editor takes a paid seat. Workspace architects and the space's own architects manage its members (`PUT` and `DELETE /api/spaces/<space>/members/<account>`, `weave_update_space` with `access`, `member`, `role` and `removeMember`). An agent token can be scoped to one space (`createAccount` with `space`), never reaches past the account that issued it, and writes an audit entry for every change it makes. A full JSON export needs access to every space (Feature #141).
- `scripts/quality-scorecard.mjs` prints one JSON row per ISO week: generalization rate of UI fixes, repeat fixes per rule class, coverage per probe over the state catalog, field types seeded, width bands covered, ceiling totals, spacing and z-index token shares, reverts on main, and the probe answers on Issue rows. The nightly flicker routine writes it to the `Development/Scorecard` table (Issue #1120).
- A bug report filed from the web UI asks **Which probe would have caught this?**, for whoever fixes it to answer (Issue #1120).
- `weave_query`, `POST /api/tables/<table>/query` and `GET /api/tables/<table>/entities` answer 200 rows a page when the caller names no `limit`, and the answer carries `nextOffset` while rows remain, so an agent pages instead of pulling a whole table. `limit: -1` reads the table whole, as on `weave_audit`. `weave_query` also defaults `relations` to `chip`, sending each related row's summary once; `relations: "full"` keeps the embedded summaries. On the weave workspace's 3,980-row Case table, a `weave_query` with no limit drops from 18,915,798 bytes to 483,895, and the same REST call to 1,582,427 (Issue #1267).
- The workspace rail lists workspaces in the order this browser last opened them, newest first, and the workspace you are in always sits first. Each workspace page load moves its name to the front of a `wv_recent` cookie (fifty names at most, same domain, path and lifetime as `wv_rail`), and `GET /api/workspaces` answers in that order; workspaces the cookie does not name follow in their usual order. The pinned weave mark keeps its place. A workspace whose session has expired shows as a grey dashed chip titled "<name>: session expired, click to sign in" and still opens through sign-in (Feature #349).
- With a base domain set, clicking another workspace in the workspace rail opens it on its own host (`<slug>.<base domain>`) instead of serving it under `/w/<id>/` on the current host. The chip goes through `GET /api/auth/open/<slug>`, which hands the current session to the other host through the single-use sign-in code, so you arrive signed in; with no session that opens it, you land on the bare host and sign in there. A page link to `/w/<slug or id>/…` asked on another workspace's host now answers 301 to that workspace's host, so old links and bookmarks land right. Without a base domain nothing changes (Issue #1263).
- **Date ranges fit their box on board tiles and the phone record page** (Issues #1126, #1162): a range input sizes to its value outside the grid, a tile gives its range the full card row, and a phone field row stacks the label over a range, so "Jan 1, 1999 – Dec 31, 2099" reads whole instead of running past the input. The grid cell keeps scrolling (Issue #897).
- The README opens on a fifteen-second clip under its lead paragraph: an agent builds a workspace from a spec with `weave build`, the table fills, a row opens with its document, then an MCP `weave_query` call returns rows. `scripts/record-demo.mjs` films it from a scratch workspace on a local serve and writes `docs/demo.gif` and `docs/demo-poster.png` (Feature #293).
- CI jobs run on `ubuntu-24.04` by name. GitHub moved the `ubuntu-latest` label to ubuntu-26.04 on 2026-10-28, where the runner has 7.8 GB free against the test manager's 10 GB floor and the cached Playwright 1.62.1 WebKit build does not launch, so every PR gate lost five of seven shards before its tests ran; the scheduled compatibility matrix stays on `ubuntu-latest` (Issue #1256).
- **Automations fire on a schedule** (Feature #312): a Workflows row's Script takes `trigger: {"type": "schedule", "cron": "0 9 * * 1-5", "zone": "America/Los_Angeles", "where": [["State", "!=", "Done"]]}`. The five-field cron expression is read on the zone's wall clock (UTC when no zone is given), and at each due minute the rule runs its actions on every live row of its table, or on the rows the `where` filter admits, as the automation actor, stamping Health and Last Run per run. The server ticks every workspace once a minute (`src/schedule.js`); a due minute fires once, a box that slept catches up with one run going back seven days at most, a new rule starts at the first tick that sees it, and a rule switched On after its minute waits for the next one. A bad cron, zone or where clause reads Setup incomplete with the reason. REST, MCP (`weave_create_automation`) and the CLI take the trigger as they take the others; the relation map pill reads `cron 0 9 * * 1-5`. Gate: `test/automation-schedule.test.mjs`.
- Added the Share beta panel with contextual read links, anonymous submission links, previews and icon actions. Links support expiration presets, custom dates, Never, and regeneration that invalidates the previous URL. Workspace UUID addresses survive renames. Submission capabilities validate before every write and retry, keep receipts private, and cannot be bypassed through the form id. Named email/domain access and automatic observer membership remain deferred (Feature #343).
- The Share (Beta) button on a table toolbar draws an icon again. It asked for `share`, a name the bundled icon set does not carry, so since Issue #1235 it drew nothing. It now takes `upload` (Issue #1233).
- Share is one button and one pane everywhere. The Share up-arrow now sits in the header of the workspace, every space, table, record page, docked record and share page, beside the ⋮ and in its borderless style, and opens the docked share pane for that page. The old Share… dialog and the Share… rows on the table and record menus are gone. A record's pane lists the table and space links that already reach it, a second page's Share button takes the pane over instead of only closing it, and the button carries a 44 px touch target on phones (Issue #765, Issue #1222).
- A narrow docked record keeps its breadcrumb inside its box: once the middle of the trail has folded into "…" and the crumbs left still overflow even at their minimum widths, the last-but-one folds into "…" too, so the current crumb keeps its #id and part of its name. A phone header still squeezes its space crumb before folding anything (Issue #765).
- On a phone the header Share button is a 44 px glass circle beside the ⋮ on the workspace home, spaces and docked records, so it answers a full-size tap on both axes instead of a 16 px wide strip (Issue #1222).
- Selected-entry links now omit related and computed fields from JSON, HTML and document exports, and query responses no longer carry counts or chip metadata from outside a narrow grant. Share API requests recheck the credential after receiving a request body (Issue #1207).
- The sheriff takes no action on a main `tests` run that ended cancelled, skipped, neutral or any conclusion other than failure, and logs the conclusion it ignored. A failed run whose only red jobs were cancelled is no signal either, and a `CI gate` failure carried only by cancelled shards counts as no verdict for a rerun or a bisect probe. A test file the in-run retry turned green is no longer named as failing, and a red held only by files that already have an open `flakes on main` row escalates to a person instead of reverting a pull request. (Issue #1139, Issue #1282)
- The sheriff no longer reverts a pull request when its rerun is cancelled, timed out, skipped or never reaches its `CI gate` job. Only a `success` or `failure` gate decides guilt; anything else is rerun once more and then handed to the next red main run. It also re-checks that main has not gone green on its own immediately before it opens the revert, and a revert whose range took no bisect probe names that in its subject. (Issue #1139)
- A sheriff revert now sets the rows it undid back to work: it reads the `Issue #<n>` and `Feature #<n>` references off each reverted commit's subject and moves each row from `Fixed` to `Open`, or from `Shipped` to `Building`, with a dated line naming the revert PR and the commit it took off main. A row in any other state is left alone, and a dry run writes nothing (Issue #1297).
- The sheriff names only the test files that stayed red after the in-run retry, so a file the retry turned green and a file whose shard ended before its retry answered no longer reach the Issue title. The unresolved files are listed under their own heading in the evidence instead. A bisect probe that goes red on a file other than the one holding main red is no longer read as a narrowing signal: the sheriff escalates with both file lists rather than convicting the commit the probe landed on (Issue #1282).
- System returns to the foot of the sidebar, under your spaces and **+ New space**, with Workflows, Users, Activity and Trash as rows that fold from the System caret. The workspace rail holds workspaces only again. The edge peek timing stays (Issue #908).
- `weave serve` checks its identity provider credentials at start and every 15 minutes. It sends a made-up code to the token endpoint with the configured client id and secret: `invalid_grant` means the provider accepted the client, while `invalid_client`, `invalid_scope` or `unauthorized_client` mean sign-in is broken. `/api/health` reads the result as `auth`. Anonymous callers see only `auth.ok`, and architects also see the status, the provider's error code and the time of the check. The top-level `ok` stays true, so a rotated secret never fails the Railway health check or rolls back a release, and the promote workflow writes one alert line when it lands a commit while `auth.ok` is false (Issue #1178).
- When the identity provider refuses weave's own client (`invalid_client`, `invalid_scope` or `unauthorized_client`), the sign-in callback shows "Sign-in is broken on weave's side" with no retry button and logs an ALERT line, on the workspace sign-in and on `/start` alike. Every other failed sign-in keeps "Back to sign in" and adds "Use a different account", which ends the provider session or asks it for a fresh login, so a live provider session no longer approves the same failing account again (Issue #1179).
- `scripts/signup-e2e.mjs` and the `signup-e2e` workflow walk production sign-up the way a visitor does: a Clerk test user signs in through a one-time link, clicks Start with weave on grunion.ai/weave, creates a workspace on `/start`, gets the unpaid 402 and the billing page, with a screenshot per step; the workspace and the Clerk user are deleted afterwards. It runs on demand until the grunion.ai link points at the sign-up entry (Feature #334).
- `GET /api/spaces/:id/home` returns a space's tables with live row counts and the automation runs that changed its rows, wherever the automation started. A run from a space the reader cannot open stays off the list. A share link gets a 403.
- An `automation-ran` activity entry records `failed: {step, error}` on the row where the run stopped.
- The recents list now holds saved table views and the time each item was opened. The command palette still shows the newest eight.
- Opening a space on desktop or tablet lands on one continuous home page: direct table links with row counts, your recent views, your recent rows beside the automation runs that changed rows here, then a Space overview disclosure holding the description, summaries, tables and relation map. Empty sections disappear. A run opens its affected rows, and a partial run names its failed step.
- The space home keys what it lists by table name and no longer says rows: **Recent** groups what you opened under each table, and a run reads `Ops → Sales · Deal 2, Contact 1`.
- A space opens as a launcher on a phone: a search box scoped to the space, one tile per table with its count, recent views and records, automation runs that changed records here with any failed step, and About this space for the description, summaries and relations. Sections with nothing to show disappear, and every control is at least 44 px (Feature #351).
- The phone launcher labels records by table name, matching the desktop space home: "Deal 2, Contact 1" on a run and "Deal #48" on a recent record (Feature #351).
- Board: a stacked layout with two or more grouping levels now draws every level, each with its own header and count, and nests the cards in the innermost group. Only the innermost groups were drawn before, so a repeated inner label gave no way to tell which outer group it belonged to (Issue #1204).
- New workspace setup now reaches its name step in a browser that refuses cookies. `/api/start` answers with a PostHog key wherever analytics is configured, so `boot()` started the consent banner before it drew the first step, and the consent code read `document.cookie` without a guard. In a sandboxed browser that read throws, and because only the `/api/start` fetch sat inside a `try`, the throw escaped and the page kept its initial "Loading weave…" status for good, with `GET /api/start` as the only request in the network log. Reading and writing the consent cookie now tolerate a browser that refuses it, and a failed analytics start can no longer stop the setup flow from rendering (Issue #1261).
- The chosen workspace row sinks and darkens the moment it is tapped, in both themes, so the opening state is visible as well as named (Issue #958).
- Opening a workspace link from the start page, the workspace switcher or an invite while signed out now lands on the sign-in page, and signing in goes on to the workspace. Before, the browser showed a raw "Sign in first" error with no way back. A signed-in person whose account cannot open that workspace gets a page that says so, with a link to their own workspaces (Issue #1284).
- **Every automation action yields an output, and later actions read it** (Feature #313): `set-field` yields `{field, value}`, `append-doc` `{field, text}`, `add-comment` `{id, text}`, `upsert-row` `{id, publicId, name, created}` and `webhook` `{url, sent}`. A later action reads an earlier one as `{{steps.1.publicId}}` in its text, in a `set-field` string value (which now takes the same `{{…}}` templates as text actions) or as `steps.1.name` in an `upsert-row` source path, so "open the account, then write its number back on the request" is a three-action rule. A step not yet run, or a path it has not got, reads empty. The `automation-ran` activity entry carries each row's outputs under `steps`, in action order. Gate: `test/automation-steps.test.mjs`.
- A guard holds the table-open query to one page with chip relations and a field projection, so a 2,000-row table opens in 234 KB instead of the 5 MB whole-table payload that froze the tab (Issue #1194).
- A test that needs a scratch directory calls `tempDir(prefix)` from the new `test/lib/temp.mjs`, which removes every directory it opened when the process exits, and all 218 bare `mkdtempSync(join(tmpdir(), ...))` calls under `test/` now go through it. A suite run under the test manager was already clean, since the manager gives each job its own `TMPDIR` and removes it; a run outside the manager kept the caller's `TMPDIR` and left hundreds of `weave-*` directories behind, which starved the 10 GB disk floor the manager needs to admit a browser lane. Gate: `test/temp-dirs.test.mjs`. (Issue #1083)
- **Board tile titles wrap instead of ending in an ellipsis** (Issue #1123): a long card title now wraps inside the tile on desktop and phone, and the tile grows to hold it, so the full name reads without opening the record.
- A toggle's label now wraps beside its switch on board tiles and on the record page instead of ending in an ellipsis. "Published to every customer" lost 81 px on a desktop tile and 8 px on the phone record page; the table cell keeps its one-line label because the column floor is measured from it (Issue #1156).
- **Move to trash answers in the first frame** (Issue #1058): the selection bar's trash button waited for every DELETE before the rows left the grid, about 0.9 s with the API held 1 s. The chosen rows, the selection bar, the loaded count and a dock open on one of them now leave in the same frame as the press, the "Moved to trash" toast with Undo shows at once and the deletes run in the background in parallel; a refused delete puts the rows back with an error toast and drops the Undo when nothing reached the trash. Undo and ⌘Z wait for a pending delete before they restore and skip the grid reload the delete would have run, so pressing Undo before the server answers goes straight to the undo and still brings every row back. The record, table, space, view and field "Move to trash" menu rows show their toast at once as well and queue Undo behind the pending delete. `trash-undo|desktop` drops from 1200 to 120 ms in `test/feedback-budget.json`. Gate: `test/trash-optimistic-browser.test.mjs`.
- A text, email or key value now rests as wrapping text on a board tile and on the record page, so a long unbroken word breaks anywhere and shows in full instead of running out of its input; a click on the text opens the input, as it does for text with inline marks. The grid keeps its one-line cell with the ellipsis at rest (Issue #1161).
- The View pane now sits rightmost, after the entity and context panes, with the same 16px gap every pane keeps between neighbours (Feature #340).
- The full-screen file viewer no longer shows a Back button beside its close ✕. It did nothing for an HTML file, whose sandboxed frame keeps its history out of reach, and only repeated the ✕ for every other kind. Refresh, previous and next, open in a tab and ✕ stay; Esc still closes (Issue #1174).
- While a file is open in the side panel or the full-screen viewer, the floating Report a problem button steps aside and the same button sits in the viewer's own bar, so it no longer covers the file's controls in the bottom corner. Its report panel opens under the bar button, and Esc closes the report panel before the viewer (Issue #1193).
- Deleting a workspace from Settings opens its own **Delete workspace** dialog. The dialog names the workspace and what moves with it, and its Delete button stays off until you type the workspace's exact name. Cancel, Escape or a click outside closes it with nothing deleted, and the message after a delete still carries Undo (Issue #1171).
- **Delete workspace… is in the workspace menu, and it is permanent** (Feature #347): the ⋮ beside the workspace title and a right-click on a rail chip (or a click on the current one) open the same menu, and its one row opens the **Delete workspace** dialog. The dialog says the delete cannot be undone and arms only on the exact name. The workspace leaves the rail for everyone, its address answers 404, and there is no Undo and no Trash entry; the hub keeps a tombstone only the operator can restore. The row shows only where you may delete that workspace, so the default workspace has no menu. Deleting the workspace you are in opens the next workspace on the rail; deleting your last one lands you on the weave workspace, which a sign-in whose only workspace was deleted still reads as an Observer.
- **Invite people… and Link preview before sign-in left the workspace menu** (Feature #347): invite from the **Invite** button beside the workspace title, and turn the link preview on or off with its switch on the **Settings** page (or `weave workspace link-preview`).
- Each workspace owns its System registry and Users table. Relations to users, forms, workflows and registry edits stay in that workspace. Hub routing no longer grants access through another workspace's architect account (Issue #979, Issue #1224).
- Existing hub registries migrate with current row IDs retained, references remapped and destination data saved before source rows become inactive. Unsupported cross-workspace references and protected workspaces without local accounts stop migration before writes.
- Workspace creation gives its creator a local account and follows hosting signup and billing rules. Any ordinary workspace can be deleted while another live workspace remains; routing aliases survive full backup and restore.
## v0.4.109 (2026-10-25)
- A field or document write inside one create or update marks its row dirty, so a value applied after a nested save in the same call reaches disk. An automation created enabled now reads as enabled after the workspace is reopened, and still fires (Issue #1085).
- The billing page draws its muted summary and back link at 5.11:1 on the canvas, up from 4.31:1, by reading the app's own light canvas-muted shade (Issue #1097).
- Danger labels read 7.45:1 on the dark panel, up from 3.69:1: the dark theme now carries its own `--tblr-danger` instead of inheriting the light red (Issue #1099).
- The `tests` and `dev-soak` workflows bound each `playwright install --with-deps` attempt at five minutes and retry it once, the way `pr.yml` already does, after a trickling mirror held that step for thirty minutes and cancelled two jobs of the `CI gate` on main (Issue #1107).
- The list layout prints each shown field's label at full strength, so it reads 5.9:1 against the row in light instead of 3.4:1 (Issue #1096).
- The Claude Code plugin manifest carries the Anthropic directory listing fields: the weave icon and the docs, support, privacy and terms links, so a directory submission reads them from `plugins/weave` (Feature #332).
- The PR browser tier now selects by the DOM classes and route literals the changed blocks name, not by file name alone, so a change to a shared module such as `public/app.js` or `src/routes.js` runs the journeys that exercise it before it merges (Issue #1075).
- Each `playwright install --with-deps` attempt in a PR browser job is bounded at five minutes and retried once, and apt gets a twenty-second acquire timeout with three retries, so a trickling mirror costs a retry instead of the `PR gate` (Issue #1107).
- The form builder's intro and field hints read 8.7:1 on the dark panel, up from 3.56:1: `.text-muted` now takes weave's own secondary token instead of the vendored fixed grey (Issue #1098).
- Every door emits one permalink per row: `url` is `/w/<workspace>/e/<uuid>` on API reads and creates, MCP row reads and verbose writes, CLI output, chips and cards, every search hit and the bug reporter's receipt. Tables and spaces answer their `/w/<workspace>/t/<id>` and `/s/<id>` permalinks, views `/w/<workspace>/#/view/<id>`. Every row parameter takes a pasted permalink, its path, `/e/<uuid>` or a bare uuid, and search ranks the row a pasted link or uuid names first (Feature #203).
- **View opens a pane beside the table** (Feature #340): the view picker, then Filters, Sort, Group, Fields and Display as an accordion, with Reset view and Clear all at its foot. The toolbar keeps Search, View and the table menu; the View badge counts active filters. Sort takes three levels, Group five nested levels in Table and List layouts, with subtotal rows per group (Every level, Top level, None) that stay on collapsed groups. A field sits in Sort or Group, never both; empty values sort last and group under No value; a multi-select row appears under each value and counts once above; deleted rows show struck through and never count. The server computes every subtotal: `weave_query` with a grouped view returns `groups` and `groupTotal`, `weave_stats` with `by` carries the same rollups, and the grid draws from that one answer. The phone table menu gains the same sort and group levels. Gates: `test/view-pane-combinations.test.mjs` (1,920 setups, 14 checks each, rule tests) and `test/view-pane-browser.test.mjs`.
## v0.4.108 (2026-10-23)
- The accessibility gate scans 13 routes in both themes (tables, board and list views, the entity page with its editor, the map, settings, trash, activity, the forms builder, a published form, start and billing) plus six open states on a table (field dialog, command palette, filter and sort popovers, share dialog, row menu), and holds today's 17 known violations in a ratcheting `test/a11y-baseline.json`, each tied to its Issue row (#1096 to #1101) (Issue #1081)
- **The activity feed answers one page by default** (Issue #940): `GET /api/activity`, `weave_activity` and `weave activity` returned every event a workspace holds when the caller named no `limit` (26,067 rows and 13.7 MB on the weave workspace). `activityFeed` now answers its newest 500 when no limit is given, the page the web UI already asks for (PR #289), so all three surfaces share one default; `limit: -1` (`--limit -1`, `?limit=-1`) still reads the whole log, and `total` keeps counting every event so `offset` pages through it. The `weave_activity` description, the CLI help and the `WEAVE_MAX_ROWS` handbook row say so. Gate: `test/activity-default-page.test.mjs` (engine, route, MCP and CLI).
- **Activity filters read as chips at rest** (Issue #980): only the selected filter had a background, so Description and Fields read as headings. Every filter now carries the soft chip (`--tblr-bg-surface-secondary`) and the selected one the primary tint, in both themes, with the 44 px phone targets kept. Gate: `test/phone-activity-sheet-browser.test.mjs` (both themes, phone and desktop).
- **The Activity head names its record** (Issue #979): the head read only "Activity", and on a phone the record sits hidden behind the sheet. It now reads `<record name> › Activity` on every width; on a phone the record crumb is a 44 px button that closes the sheet and takes back its history step. Gate: `test/phone-activity-sheet-browser.test.mjs`.
- **History follows an account across a rename** (Issue #699): a signed-in request set the actor to the account's name, and audit lines, activity, `createdBy`/`modifiedBy`, document revisions, comments, share links, invites and credential owners stored that string, so renaming `kyle` to `kyle.adriany` left every earlier line naming an account that no longer existed. A signed-in write now stores `account:<id>` (and `account:<id> via MCP` over OAuth), and every read path (the API, MCP, CLI and the web UI through them) renders the account's current name, or `removed account` once it is deleted. Lines written before this change are not rewritten and keep rendering the name they hold. Credential grants now record the account id too, and a credential owned or shared under an account's name still opens for that account. Plain writers (`web`, `local`, `mcp:<client>`, `bug-report`, `share:<label>`) are unchanged. Gate: `test/actor-account-id.test.mjs` (create `kyle`, write a row, a document and a credential, rename to `kyle.adriany`; history and reveal both follow the account).
- **The + New row stays the grid's last line with a selection open** (Issue #986): in a grid wider than its card, the card is its own scroller, and opening a selection gives it 62 px of bottom padding so the last rows clear the selection bar. The sticky + New row stuck to the top of that padding, so a band of rows showed underneath it and the add line read as floating mid-table. The band under the foot is now painted with the surface, as the pane's padding already was in a grid that fits, so no row shows below + New row while scrolling, and at the end of the table it rests under the last row. The tbody order was already right in every path (scrolling, a later page, sorting, a created row, a selection), and the gate now proves it. Gate: `test/sticky-add-row-browser.test.mjs` (both grid widths, Chromium and WebKit).
- **API reads revalidate instead of refetching** (Issue #1004): every `GET` under `/api` that answers 200 now carries a weak `ETag` hashed from the exact body that request gets, with `Cache-Control: private, no-cache`, and a matching `If-None-Match` answers 304 with no body. The browser's own HTTP cache revalidates `/api/schema` and table pages on every navigation back, so an unchanged 50-row page costs a header exchange instead of its full body. Because the tag hashes the caller's own body, a tag from one member never answers another member's narrower read, and the auth wall still answers before any 304. Writes, `POST /query`, errors, `no-store` answers and cookie setters are left as they were, and private answers stay out of the shared compression cache. Gate: `test/api-etag.test.mjs`.
- **A refused schema apply writes nothing** (Issue #996): `applySchema` creates spaces and tables as it walks the document and only then checks for deletions, so a document that omitted an existing space raised "would delete space" after its new spaces already existed. The real run now runs the dry run first, so a refusal throws before the first write, and `weave_apply_schema` without `allowDestructive` leaves the workspace as it found it. Gate: `test/schema-apply.test.mjs`.
- **Attachment cells and covers load a small preview** (Issue #1005): a 4 MB phone photo in a small attachment cell used to download all 4 MB, because weave kept only the original. The web app now draws each PNG, JPEG or WebP upload to a canvas at upload (longest side 320 px, WebP, JPEG where the browser cannot encode WebP) and sends it beside the file; the file entry records it as `preview: { id, size, mime }`. `GET /api/files/<id>?preview` serves it, and answers the original for a file without one, so files uploaded earlier, and uploads from the REST API, MCP or CLI, keep working. Sheet cells and the entity cover ask for the preview with `loading=lazy` and `decoding=async`; the cover swaps to the original once it renders wider than the preview, and the viewer and lightbox always load the original. Previews count toward storage, travel in exports, imports and scoped backups, and go when their file or row is deleted. Gate: `test/file-preview.test.mjs`, `test/attachment-preview-browser.test.mjs`.
- **The audit log names the real caller** (Issue #495): a request with a token or session records its account and the `x-weave-actor` header is ignored, even when another request names itself while the first one waits on billing; an anonymous caller on a workspace or hub that holds any account is recorded as `anonymous` and its comments carry `anonymous` as author. On a hub with no accounts an anonymous caller still names itself through `x-weave-actor`, as local agents and the CLI do. CSV import, JSON export, JSON import and workspace create, trash, restore and delete each write one audit entry (`csv-imported`, `workspace-exported`, `workspace-imported`, `workspace-created`, `workspace-trashed`, `workspace-restored`, `workspace-deleted`). Gate: `test/security/audit-actor.test.mjs`.
- **Avatar field type: a row's face on its chip, card, crumb and header** (Feature #349, supersedes Feature #139): a new `avatar` field holds one image file from the row, or nothing, in which case the row shows its initials on a colour drawn from its name. `shape` is `round` (default, people and agents) or `square` (organisations, logos), and a table holds one. A table with an avatar field puts the face first in the row's chip inside its link, first in the card's head, in the crumb in place of the row mark, beside the name on the row's page (click or drop an image to change it), in relation chips that point at the row and beside the ⌘K hit. A table without one renders as before. System/Users is minted with an Avatar field, and an agent account wears a small badge. The `PERSON_TABLE` name regex that guessed people from table names is gone. An avatar never takes an inventory icon: an icon says what kind of row, an avatar says which row. Gates: `test/avatar-field.test.mjs`, `test/chip-renderer.test.mjs`, `test/breadcrumbs.test.mjs`. The upload takes PNG, JPEG, GIF or WebP; an SVG is refused because weave serves uploaded SVGs as downloads.
- **The backup round trip is compared field by field** (Issue #1078): `test/backup.test.mjs` seeds a workspace with every field type the engine offers, two document revisions, an attachment, an avatar, a comment and an automation, backs it up, restores it into a fresh directory and deep-compares the whole workspace, each revision and each blob by sha256, in plain and encrypted form. It also covers a wrong-passphrase restore that writes nothing, `streamTar`, `tarWriter`, `classify` and `heldOpen`, and restores a checked-in archive written by release v0.4.100. Tests only; no change to `src/backup.js`.
- **Compare two to four picked Board cards in the pane row** (Feature #354): pick cards with the box that shows on hover or focus, with Shift-click, or on a phone with a long-press, then **Compare N** in the footer. The pane beside the board draws one column per card with its cover on top and the view's shown fields aligned row by row; a row whose values differ is tinted, and every value edits in place. A fifth pick is refused with a footer hint, Esc closes the pane, and on a phone the pane is the full-screen sheet that scrolls sideways one card at a time. Shift-click on a card picks it instead of opening a new tab. Gates: `test/board-compare.test.mjs`, `test/board-compare-browser.test.mjs`.
- **Drop a picture on a Board card to set its cover** (Feature #353): a desktop file dragged over a card lights it with `is-file-drop`, and the drop uploads the first picture into the row's cover field through the attachments editor's own upload path, then repaints the card. A file that is no PNG, JPEG, GIF or WebP is refused with a toast naming it, and a table with no cover field offers **Add a Photo field**, which adds a single-file cover field and uploads the picture into it. An upload into a single-file attachments field that already holds a file now replaces it, keeping the old file in the row's ledger, where it used to fail with "holds one file"; the attachments handbook page says so and describes the card drop. Gates: `test/board-cover-drop.test.mjs`, `test/board-cover-drop-browser.test.mjs`.
- **Board layout: kanban columns and a picture gallery in one table layout** (Feature #352): a view takes `layout: 'board'` beside Table and List, with `groups` (`stacked` sections or `side` columns, default `side`) and `picture` (`off`, `small`, `medium` or `large`, default the cover field's size; cards 132/188/256 px, columns 184/232/288 px, three small cards per row on a phone). The picture is the record's cover, the first shown attachments field set to Show as cover, through one `coverOf(db, entity)` in `public/board-core.js` that the record page now shares. The View menu shows Board with Columns and Gallery presets and Groups and Picture controls. Cards drag with `RO().sortable`: into another group sets its value with Undo, within a group or with no groups saves the view's order in reading order, and a sort turns hand order off with a Manual button in the footer. Card fields edit through the table cell editor, and a card click opens the record in the pane row with the open card outlined. A query that names an attachments field now returns that field's files. `weave_table_view`, `weave table view --groups --picture`, the schema document and the Views registry (Groups, Picture) carry the new keys. Gates: `test/board-layout.test.mjs`, `test/board-layout-browser.test.mjs`.
- **Board layout opens on any click and fits a phone** (Feature #352): a click anywhere on a card (cover, title, a field label, blank space) opens the record in the pane row, and a click on a field value also opens that field's editor; a press that moves is a drag. On a phone (600 px and under) Stacked shows 3 small or 2 larger cards per row, Side by side snaps 85vw columns under sticky headings, a 250 ms hold lifts a card while a swipe scrolls, a lifted card auto-scrolls on both axes (`RO().sortable` takes `alsoScroll`), every card target is 44 px tall, and a tap anywhere on a card opens the full-screen sheet. Gate: `test/board-layout-phone-browser.test.mjs` (Chromium at 375 × 812 and WebKit iPhone 15).
- **Swimlanes on a Side by side Board** (Feature #355): a second group level now draws as horizontal lanes across the columns instead of being ignored. Each lane heading sticks to the left edge and folds through the view's collapsed group paths, so the fold matches Stacked and List. Dragging a card into another lane writes the second group field and into another column the first, with Undo. Empty lanes follow empty groups as columns do, and a search keeps only the lanes that hold a card. Stacked keeps its nested sections. On a phone the columns snap at 85vw under sticky headings and the lane fold is a 44 px target. Gates: `test/board-swimlanes.test.mjs`, `test/board-swimlanes-browser.test.mjs`.
- **The home boot no longer waits on a second schema build** (Issue #395): the page shell now carries `<meta name="weave-workspace" content="<name>" data-root="true|false">`, so the app knows at boot whether it is on the root. The root, including `/w/<root>/`, reads its schema once instead of also fetching `/api/schema` as a registry it would throw away; a member still reads the root registry beside its own schema (the Issue #258 guard). `/health` and the home's `/workspace`, `/workspaces` and `/invites` reads now start beside the schema instead of after it. A shell without the marker falls back to the old speculative registry read. Gate: `test/schema-fetch-browser.test.mjs`, `test/workspaces.test.mjs`.
- **The bug panel's step counter reads in full** (Issue #939): the "N steps captured" line in the report footer was clipped to an ellipsis on macOS, where the monospace face measures three pixels wider than the footer allowed, so every local cut-off gate run on a Mac went red on it. The counter now wraps when the footer is narrow instead of clipping. Gate: `test/cutoff-gate-browser.test.mjs`.
- **Every route, MCP tool and CLI command has a test that calls it** (Issue #1080): `test/census.test.mjs` derives every route from the dispatcher in `src/routes.js` and the passcode applet in `src/applet.js` (method by method, so `PATCH` and `DELETE` on one path count apart), every tool in `src/mcp.js`'s `TOOLS` and every command in `bin/weave.js`'s command table, then fails naming each one no file under `test/` calls. The allow-list is empty. New call tests cover link and unlink, automation `PATCH` and `DELETE`, attachment `DELETE`, the applet's `PATCH /t/entity/:id` and `POST /t/entity/:id/doc`, key share `DELETE`, `GET /api/spaces/:id`, the `/api/relation-map.mmd` flowchart, `weave_check_formula`, `weave_import_csv`, `weave_restore_field`, and the `audit`, `undo`, `quality`, `delete`, `formula` and `key` commands. Three defects they found are pinned at today's behaviour and filed: Issue #1086 (a key share `DELETE` naming an account answers 200 and revokes nothing), Issue #1087 (a second `DELETE` of a file or automation answers 200) and Issue #1091 (`weave audit --limit` with a word fails with SQLite's "datatype mismatch").
- **weave installs from a Claude Code plugin marketplace** (Feature #340): the repository now carries `.claude-plugin/marketplace.json` with one `weave` plugin whose `.mcp.json` points at `https://weave.grunion.ai/mcp` over HTTP, so `/plugin install weave --marketplace grunion-ai/weave` connects Claude Code and signs in through Clerk on first use. The plugin carries no version, so Claude Code follows the repository commit and no release has to restamp it. No `.mcp.json` sits at the repo root, so a session developing weave is never wired to production. README names the install line. Gate: `test/claude-plugin.test.mjs`.
- Sign-in clears its busy accessibility state when the embedded form finishes loading (Issue #960).
- **The Cloudflare Workers adapter is gone; weave runs on node alone** (Issue #231): `src/worker.js`, `src/store-cf.js`, `wrangler.toml.example` and their two suites are deleted, since the uno phone inbox they hosted moved to the production instance on 2026-10-15. The workerd guards they forced come out with them: `src/mcp.js` reads its version and primer once at load, `src/pdf.js` resolves the font path without a try, and `src/routes.js`, `src/mcp.js` and `src/mail-send.js` read `process.env` directly. The Worker cases leave the legal-pages, forwarded-for, internal-error-body and doc-revisions suites, the semgrep rules stop naming `src/worker.js`, and the architecture map and the "Registry at the root" Handbook guide drop the Workers host. Gate: `test/node-only.test.mjs`.
- **A query computes each formula, lookup and rollup once per row** (Issue #1000): the sort called the field resolver inside the comparator, about 2n log2 n times, and the page read resolved every value again. The sort now computes each row's key once and sorts the pairs, and a query keeps a memo of computed values keyed by row and field, dropped when the query returns, so the next query sees every write. A value computed under a cycle or past the depth limit is never kept, and a kept value is reused only at the depth it was computed at or shallower, so results match the uncached read. Sorting 5,000 rows by a formula over a lookup went from 277.8 ms to 29.5 ms; the 1,000-row grid over a filtered rollup from Issue #999 went from 0.9 s to 77 ms. Gate: `test/computed-memo.test.mjs`.
- **Activity opens in the context pane, the next card in the pane row** (Feature #348): a new `#side` card sits after `#dock` with its own `#side-gutter`, built from the entity pane's parts. It is the same card (16 px radius, panel shadow, 8 px margins), right of the record on the full page and right of the entity pane in the peek, so it covers neither. Its width defaults to 340 px; dragging the gutter pins it (min 300 px, kept as `wv-side-width`) and a double-click resets it. The URL carries `side=activity` beside `?e=`, so a reload or a shared link reopens it, and ⌘⇧E between the full page and the peek keeps it open. Esc closes it before the entity pane, and closing the record closes it. On a phone it is still the full-screen sheet with a history step. This replaces the pane docked inside the record view (Issues #871, #906): the record keeps its full width and the margin-right offsets are gone. `pinDock`, `applyDockWidth` and `wireDockGutter` became `pinPane`, `applyPaneWidth` and `wirePaneGutter`, shared by both panes. Gate: `test/activity-panel-placement-browser.test.mjs` (6 cases), `test/phone-activity-sheet-browser.test.mjs`.
- **Copy permalink keeps its 44px phone target beside the header dots and Invite** (Issue #853): with the page ⋮ pinned to the crumb row, a workspace name long enough to push Copy permalink to the end of the breadcrumb left it within 11px of the ⋮ or the Invite button, and the phone touch-target check failed on WebKit in CI (40.5px wide). On phones and touch screens the copy button now keeps 11px clear on its trailing side, so its 44px hit area never runs under the next control. `test/phone-crumb-copy-target-browser.test.mjs` sweeps workspace names from 12 to 40 characters on Chromium and WebKit.
- The cut-off gate treats `test/cutoff-baseline.json` as a ceiling: a count over it still fails, and a count under it is reported as a stale baseline instead of failing the run. One breadcrumb fingerprint measured 5, 7 and 8 instances on the same commit, and the dropped-count half of the ratchet turned that variance into a red `CI gate` on main. The baseline still only ever shrinks, through `node scripts/cutoff-sweep.mjs --lower` and the guard that refuses a baseline which grew. (Issue #1103, Issue #1106)
- **Decks render in a sandbox** (Issue #493): the deck player inserts a slide's `html` and `svg` fields as markup, and `deck.html` was served on the workspace origin in an unsandboxed frame, so a script in a slide ran with the viewer's session. `deck.html` now answers under the HTML attachment view policy (`Content-Security-Policy: sandbox allow-scripts ...`, no `allow-same-origin`) and the entity view's deck frame carries the attachment frame sandbox. The deck player and the `.html` download keep working. Gate: `test/security/deck-sandbox.test.mjs`.
- **Document pages render in a sandbox, and in-app markdown escapes HTML blocks** (Issue #491): a raw HTML block in a document reached the workspace origin through six paths, so a script in a document ran with the reader's session. An HTML document, `doc.html`, `entity.html` and every share page now answer under a CSP sandbox with no `allow-same-origin` (an HTML document wears the attachment view policy; rendered pages also load weave's own scripts, styles and images so mermaid, code highlighting and chips keep working), and the entity view's HTML document frame carries the attachment frame sandbox. `POST /api/markdown` (view descriptions, reference chips) and the task applet's `docHtml` escape HTML blocks through `renderMarkdown(md, { rawHtml: false })`. Gate: `test/security/document-sandbox.test.mjs`.
- **Every editable text field shares one focus treatment** (Issue #1030): the view title, record name, grid cells and field rows, the view rename input, the nav add-table input and the description editor each painted their own focus, and the view title drew two outlines (its grey border plus the global `:focus-visible` outline, which is `!important`). All six now carry `.wv-edit`: transparent at rest, a faint border on hover, and on focus the surface background with one `inset 0 0 0 1.5px` primary ring and no outline. Padding and size come from `--wv-edit-pad` and `--wv-edit-fs` (renamed from `--wv-inline-*`), so each field keeps its type size. An empty title or record name shows "Untitled" and holds at least 8ch, the view title row keeps an 8px gap with or without a description, the title pulls left only when nothing precedes it, and the description info button sits as a 14px superscript glyph at the title's top right, with a 24px hit area (44px on touch) that starts at the title's edge, extends right and down, and leaves the row height and the title's own tap area alone. Gate: `test/edit-field.test.mjs`.
- **Document loading survives stylesheet failures and dismissal** (Issue #955): concurrent document opens wait for both editor assets, a failed stylesheet can retry after the script loads, and dismissed documents ignore pending editor loads and errors.
- Create empty workspace keeps the entered name and address and validates availability. (Issue #1010).
- **Signing out of a workspace opened through the hub root leaves the root signed in** (Issue #716): a member workspace with no account for you opens on the root workspace's session, so `POST /w/<member>/api/auth/logout` revoked that root session and signed the browser out of the root and of every other workspace the fallthrough opened. That broke the Feature #280 promise that signing out of one workspace touches no other. The logout route now revokes only a session the workspace owns. For a borrowed session it sets an `HttpOnly` `wv_out_<workspace id>` cookie instead, which stops the fallthrough for that workspace in this browser; signing in to that workspace again clears it, and Sign out everywhere clears it with the sessions. Gate: `test/auth-workspace-sessions.test.mjs` (1 new case: after a member sign-out the root answers 200 with its session intact, the member answers 401, another member still opens, a fresh sign-in reopens the member, and Sign out everywhere ends the root session and clears the marker).
- **No stray line beside # after scrolling a table back to the left** (Issue #1031): the frozen # column and the last frozen field drew their edge by recolouring a collapsed border, which the table painted where the cell started rather than where it sticks, so the edge never followed the frozen column and a `scrollLeft` that settled at a fraction (0.5 after trackpad momentum on a 2x display) left it standing between # and Name. The edge is now a 1px overlay on the sticky cell that shows while the grid sits scrolled and hides at rest, and the grid counts anything under one pixel as the left edge. Gate: `test/grid-scroll-edge-browser.test.mjs`.
- **The ⋮ stays on the breadcrumb row** (Issue #853): the page menu trigger rode at the end of the toolbar, so when the toolbar wrapped it went with it. Measured at 1470 x 794 with a row docked beside the table: at a 500 px dock the table's ⋮ sat on the second line, at 700 px on the third, at 900 px on the fourth, each time at the bottom of the wrapped controls. On a phone it left the header for the last row of the sliders sheet. `viewHeader` now places a trailing ⋮ on the crumb row itself, pinned to its right end and centred on the breadcrumb line, while search, view, density, fields, filter, group and sort keep wrapping below it. On a phone the table ⋮ is a 44 px glass button between the breadcrumb and the sliders button, and the sliders sheet no longer carries a Table actions row. Applies to every page built on `viewHeader`: table, space, workspace home and the rest. Gate: `test/header-dots-browser.test.mjs` (12 cases: four dock widths in both themes on a desktop; touch Chromium and WebKit iPhone 15 in both themes, with the sheet shut and open); selectors in eight menu suites move from `.crumb-actions .dots-btn` to `.crumb-row .dots-btn`, and `phone-table-menu-browser` now opens the table menu from the header.
- **A root session opens another workspace only where it belongs** (Issue #985): any session on the default workspace counted as signed in on every other workspace, with its root role, so a root editor or observer could open a walled workspace that never gave them an account and find its rows through `?all=1` search. A root session now crosses into a workspace when the same sign-in identity holds an account there, and acts with that workspace's role, or when it is an architect of the root, who already runs every workspace's lifecycle. Everyone else gets 401 there and no search hits from it. Gate: `test/auth-workspace-sessions.test.mjs` (a non-member workspace refused, members opened with their own role, `?all=1` scoped to members) and `test/security/hub-routes-root-gate.test.mjs` (a root reader or writer session cannot open `secret` or find it; the root admin still can).
- **Entity writes refuse a stale revision** (Issue #1076): `PATCH /api/entities/:id` takes an `If-Match` header carrying the `revision` the caller last read. When the row has moved on since, it answers 412 with `{ error, code: 'stale', entity }` and writes nothing; without the header it writes as before, so the CLI, MCP tools and scripts are unchanged. Table query rows carry `revision` as well. The grid sends the revision it drew each row from, so two people editing one cell no longer overwrite each other: the second commit repaints the cell with the first one's value and raises a toast naming the field, who changed it and the value that was not saved, with **Use mine** to commit it on purpose. A commit that cannot reach the server says so and leaves the typed text in the cell. Gates: `test/if-match.test.mjs`, `test/if-match-browser.test.mjs` (Issue #1076)
- **Invite moves to the workspace header, and the stray "null" is gone** (Issue #759): inviting someone meant finding a collapsed `MEMBERS` disclosure 2,076 px down the workspace home page, and that box printed a literal `null` under its form because `membersSection` appended the invite-link card and the seat note even when they were `null`, which the DOM writes out as text. Home now carries an **Invite** button beside the Workspace actions ⋮, and the Workspace actions menu (header ⋮, header right-click and the tray chip) gains **Invite people…** in its Share section. Both open one dialog: email, role and Invite on top (Return invites too and keeps the dialog open), then the sign-in link card when there is one, then Members and Pending invites with real empty states, "No one else has joined yet." and "No pending invites.". Every child is filtered before it is appended, so no `null` text node is left in any state. The button and the row show only where `GET /api/invites` answers, so a reader without architect rights sees neither. The Handbook's Members paragraphs now describe the dialog. Gate: `test/invites-browser.test.mjs` (6 cases, both themes: Invite sits beside the ⋮ and home has no Members box; invite, copy the link and revoke with no `null` text node at any step; the menu row opens the same dialog and Return invites), `test/invites-mail-browser.test.mjs`, `test/menu-triggers-browser.test.mjs` and `test/menu-core.test.mjs` updated for the new row.
- Used invite pages offer ordinary sign-in and existing-member recovery without redeeming the invite again. (Issue #1013).
- **The workspace header's Invite button is a 44px target on a phone** (Issue #759): moving Invite into the workspace header left it a 42x28 `btn-sm`, which `test/phone-touch-targets-browser.test.mjs` refuses on the workspace home. It now takes the same 44px floor as the other header buttons at phone width and on touch screens.
- Invited members skip creator onboarding, including architects joining an empty team workspace (Issue #1015).
- Editors and observers see role-aware empty states without unauthorized table, template or space creation actions. (Issue #1008).
- **Credential access lists hold account ids only** (Issue #497): a key's owner and its grants are stored and compared as `account:<id>`, so a caller whose actor is only a name, such as the CLI's user name, no longer opens, shares or rotates a credential owned by the account of that name. Setting a key records the caller's account id as owner, or none when the caller has no account. Sharing or unsharing a key with no account owner (one from the original keystore, or one whose owner is a bare name) needs an architect on the hub root, and sharing no longer makes the sharer its owner. A grant to a name with no account is refused. Re-grant a legacy credential with `weave key share <name> --with <account>` (or `POST /api/keys/<name>/share`) as a root architect. Gate: `test/security/key-acl-ids.test.mjs`.
- **The activity log page loads when you visit it** (Issue #1002): the `#/activity` list and the entry detail page (`showActivity`, `showActivityDetail` and their summary, record chip, field change and rollback helpers) moved out of `public/app.js` into `public/activity-log.js`, fetched on the first visit to the activity log. A row page's own activity panel stays in `app.js`. `app.js` drops by 10,397 bytes, so a cold home, table or row load downloads 10.5 KB less. Gate: `test/lazy-subsystems-browser.test.mjs` (cold loads fetch no lazy file; the activity log fetches it once, lists entries and opens one).
- **The row Activity panel loads when you open it** (Issue #1041): the panel behind a row's Activity button (its feed, filters, diffs, restore and revert) moved out of `public/app.js` into `public/activity-panel.js`. `app.js` keeps a small `activityPanel` with the same `toggle`, `close`, `mounted` and `openFor`, which fetches the file the first time someone opens the panel or follows a link that names it (`side=activity`). A cold home, table or row load downloads 15.6 KB less. Gate: `test/lazy-subsystems-browser.test.mjs`.
- **The search palette loads when you open it** (Issue #1002): the command palette (`openCommandK`, its result rows and `navigateToResult`) moved out of `public/app.js` into `public/command-palette.js`, fetched the first time someone presses the search button, the keyboard shortcut, the phone search bar, or picks a reference or relation through it. Recents (`noteRecent`, `noteEntityRecent`) stay in `app.js` because every page visit writes them. `app.js` drops by 9,994 bytes, so a cold home, table or row load downloads 10.2 KB less. Gate: `test/lazy-subsystems-browser.test.mjs` (cold loads fetch no lazy file; the search button fetches it once and finds a row).
- **The date picker loads on first use** (Issue #1041): the calendar popover (`datePopover`), typed-date reading (`readTypedDate`) and `orderRange` moved out of `public/app.js` into `public/date-picker.js`. Date and range cells still paint at once from `app.js`, and the file is fetched when someone focuses or presses a date cell, so it is usually ready before the edit commits; a typed date or a calendar press made before it arrives waits for it and then lands. A cold home, table or row load downloads 13.7 KB less. Gate: `test/lazy-subsystems-browser.test.mjs`.
- **The field dialog loads when you open it** (Issue #1002): the field settings dialog and its editors (formula builder, option and state lists, number and date display controls, sparkline and colour pickers) moved out of `public/app.js` into `public/field-dialog.js`, which the browser fetches the first time someone opens field settings, adds a field or configures a chip or card. Every route used to parse that code before it painted. `app.js` drops from 663,378 to 599,861 bytes, so a cold home, table or row load downloads and parses 63.5 KB less. Gate: `test/lazy-subsystems-browser.test.mjs` (home, table and row loads fetch no lazy file; field settings fetches it once and opens).
- **The forms dialog loads when you open it** (Issue #1041): the table menu's Forms dialog and the form editor (`formsDialog`, `formEditor`) moved out of `public/app.js` into `public/forms-dialog.js`, fetched the first time someone picks Forms… from a table menu. A cold home, table or row load downloads 10.2 KB less. Gate: `test/lazy-subsystems-browser.test.mjs`.
- **The loading mark moves while the rope is still in flight** (Issue #904): `initPageLoader()` fetches the 78 KB `/brand/weave-loader-rope.html` only after `app.js` has parsed, so on a hosted instance the loader cleared its 500 ms flash gate and sat on the static `index.html` mark with nothing running, which is what "loading animation broken" reported. Measured on `:4400` at 4a15e25: with the rope delayed or failed, `#page-loader .loader-mark` carried 0 running animations for the whole visible wait, in both themes. The inlined mark now breathes on its own, `loader-boot`, a 2 s `scale(1 → .94 → 1)` on the same cadence as the rope and transform-only like it, scoped by `:not(:has(.rope-a))` so the rope's arrival ends it and a slow or failed fetch no longer leaves a frozen mark. Reduced motion still runs nothing. Gate: `test/page-loader-browser.test.mjs` (5 new cases: a delayed rope and a blocked rope each leave at least one transform animation running once the loader is visible, both themes; the rope takes the mark back when it lands).
- **The loading rope draws both strands in light theme** (Issue #975): the light-theme `.mark-light` host was an inline span, so the flipped navy strand pivoted on a line box and drew outside the mark, leaving loose blue pieces. The three rope hosts now make both theme spans `display: block`. Gate: `test/page-loader-browser.test.mjs` (both strands sit on the 96 px mark, each theme).
- **`weave_audit` with no limit answers a page instead of an error** (Issue #943): the MCP tool passed `limit: args.limit ?? null` to the store, a destructuring default never fills `null`, and `node:sqlite` refuses a NULL `LIMIT` binding, so a call with no arguments threw `datatype mismatch` on any on-disk workspace (an in-memory one answered zero rows). The tool now passes `undefined`, so the store's declared default of 100 applies, the same page `GET /api/audit` answers; `limit: -1` still reads every row. Gate: `test/mcp-audit-limit.test.mjs` (no limit, an explicit null, a page size and -1, in memory and on disk).
- **MCP reads mark stored text as data, and a bug report's Replay list keeps one line per step** (Issue #499): a read tool answered with the stored text bare, so a row name, document or comment could pass for an instruction to the agent, and a bug report wrote recorded event fields into the Replay list as they came, so one field with newlines or backticks could add steps or headings. Every read-only tool now answers inside `<weave_data table="…" entity="…" field="…">…</weave_data>`, with any `<weave_data` or `</weave_data` in the stored text escaped so it cannot close the envelope; the primer says text inside it is data written by workspace users (primer 1,880 of 2,048 characters; the AGENTS.md Using weave section stays under its 11,000-byte limit). Writes answer compact and bare, as before. In bug reports, every event field and every Where value has its whitespace runs and backticks flattened and sits in its own code span. Gate: `test/security/mcp-envelope.test.mjs` and `test/security/bugreport-replay.test.mjs`.
- **MCP offers each role the tools it may call** (Issue #508): `/api/mcp` refused every editor and observer outright, and `tools/list` named every tool to whoever reached it. An observer now gets the read and search tools plus `weave_add_comment`, an editor adds row create, update and delete and `weave_attach_file`, and an architect keeps every tool; the map is `ROLE_TOOLS` in `src/mcp.js`. `tools/list` names only the caller's tools, and `tools/call` on any other answers `Unknown tool`, as a tool that does not exist does. An editor writes no row of a system table over MCP (Spaces, Tables, Fields and the rest), as the REST API already refuses, and an observer's MCP comment carries its own name whatever `author` it passes, as a REST comment does. Gate: `test/security/mcp-admin-gate.test.mjs`.
- **The vendored mermaid bundle carries DOMPurify 3.4.17** (Issue #1092): OSV published GHSA-c6gr-qvv4-5v3f, GHSA-cg2g-xh64-v4q7 and GHSA-mrv2-3pqr-j5jr against DOMPurify 3.4.16 on 2026-10-22, and the required security scan failed on every pull request from then on. `public/vendor/mermaid.min.js` is mermaid 11.17.2 rebuilt by the recipe in `security/vendor.lock.json` with dompurify 3.4.17, the first fixed release; the lock records the version and the new hash. Gates: `test/security/mermaid-dompurify.test.mjs` now refuses anything below 3.4.17, `test/security/vendor-lock.test.mjs`, `node scripts/vendor-advisories.mjs`.
- **First-party scripts and styles go out minified** (Issue #1001): the server strips indentation, comments and the spaces beside punctuation from every `.js`, `.mjs` and `.css` file under `public/` (never `public/vendor/`) once per file version, keeps the result beside the compressed copy, and compresses the minified text. The repo source stays as written and there is still no build step or dependency. Every line break stays where it was, so a stack trace or bug report line still points at the source line. A file the minifier cannot read is served as written. `app.js` drops from 603,912 to 509,791 bytes raw and from 130,530 to 123,882 bytes with Brotli; all 49 first-party assets together drop from 283,453 to 269,537 Brotli bytes. Minifying `app.js` takes about 35 ms, once per file version. `test/byte-budget.json` now holds the served bytes of every first-party asset after gzip and after Brotli, and refuses a raised budget that carries no reason under `raised`, the pattern `test/click-budget.json` uses. Gates: `test/minify.test.mjs` (every first-party script keeps the same token stream on the same lines, checked with the acorn tokenizer bundled in Node), `test/byte-budget.test.mjs`.
- **A cold phone load moves a quarter less and boots before the document editor arrives** (Issue #989, Net Tasks #126): every page loaded Vditor, the document editor (70 KB script and 9 KB CSS, gzipped), as render-blocking tags, and the server compressed with gzip only. `index.html` now names Vditor's files in a `<template>` (stamped with immutable versions like every other asset), and the app loads them the first time a document editor mounts, or 1.5 s after the first route paints. Compressible responses with an ETag go out as Brotli to clients that accept it: the first request still gets gzip while Brotli quality 11 compresses off the request path, and later requests get the Brotli copy from the per-ETag cache. Uncached responses keep gzip. Gates: `test/compression.test.mjs`, `test/editor-lazy-load.test.mjs`.
- Phone setup skips the hidden desktop artwork download and loads it only when the window expands to desktop size (Issue #959).
- **Four surviving mutants now fail a test** (Issue #1084): a 48-mutant sample of origin/main flipped one operator at a time and four flips passed every test. `isActor` on a removed account, `cellText` on object cells of a shared table page, the `true` formula literal and a non-Bearer Authorization header beside a session cookie each have a test that fails when the flip is applied. Tests only, no `src/` change.
- **The nav's + New table and New space buttons show again** (Issue #1026): PR #347 wrote `hidden: !state.canManageForms`, which `el()` turns into `hidden="false"`, an attribute that hides the button for everyone. Both buttons now carry `hidden` only for accounts that cannot manage the workspace. Gate: `test/icon-vocabulary-browser.test.mjs`.
- **The sidebar's pinned block holds still while the account chip loads** (Issue #944, filed twice as Issue #946): the account slot in the identity row starts empty and hidden until `/api/auth/me` answers, so the row stood 49 px tall and grew to 53 px when the chip landed, moving `aside#sidebar > div.nav-pinned` up 4 px with no input (layout shift 0.0089 on `load-home`, 3 of 3 sweeps). An empty slot in that row now keeps the chip's 32 px, so the row is 53 px from its first paint. Gate: `test/flicker-fixed.json` holds `load-home|shift|aside#sidebar > div.nav-pinned`, which `test/flicker-gate-browser.test.mjs` fails on if it comes back.
- **OAuth MCP seats a person on a member workspace only where they belong** (Issue #488): `/w/<member>/mcp` signed in through the identity provider fell back to the person's hub root account on any role, so a root editor or observer was signed in on a member workspace it never joined. The OAuth door now finds the account on the member workspace first and falls back to the root account only for a root architect, the rule browser sessions already follow (Issue #985). SECURITY.md names the hub operator's reach. Gate: `test/security/hub-routes-root-gate.test.mjs`.
- **Accounts take the person's name** (Issue #935): sign-in asks the identity provider for `openid email profile`, and `redeem()` returns the `name` claim (trimmed, at most 80 characters) beside the verified address. A workspace created at the start page names its owner's account from that name; the address local part is the fallback when the provider sends none. The `picture` claim is still dropped. The privacy policy and the Handbook say so, and `test/verified-email.test.mjs` fails when the scope weave requests and the privacy wording disagree.
- Run signup, invitation and second-workspace browser journeys on every pull request; refuse a missing required journey suite before auto-merge (Issue #1028).
- **Opening a table paints its crumb, title and toolbar before the rows arrive** (Issue #1056): on the phone, where the sidebar is hidden, a tap on a table changed nothing on screen until the server answered (1499 to 1648 ms on the PR gate with the API held 1 s; 1101 to 1164 ms locally). `showDatabase` now builds the table chrome from the schema the client already holds in the same frame as the hash change, the skeleton rows paint under that header after the usual 150 ms, and the rows fill in as they land; a table whose first page was fetched earlier in the session paints that page at once and redraws only when the fresh page differs. Measured locally after the fix: 9 to 10 ms on the phone, 6 to 13 ms on desktop. The open makes one `query` request (rows, chips, trash count and total in one call), so there was nothing to fold. `test/feedback-budget.json` holds `open-table|phone` at 120 ms, down from 2150, with its raised reason gone.
- Open a created workspace immediately; desktop AI clients live under an expandable section. (Issue #1011).
- **A page asked for on a second hostname moves to `WEAVE_ORIGIN`** (Issue #547): the hosted instance also answers at its Railway-generated name, where sign-in could never finish. The row reported a passkey bound to `weave.grunion.ai`; with the passkey door gone (Feature #243) the same name now starts a provider sign-in whose state cookie lands on the Railway name while the callback returns to `weave.grunion.ai`. With `WEAVE_ORIGIN` set, a `GET` or `HEAD` for a page on a name listed only in `WEAVE_ALLOWED_HOSTS` now answers `308` to the same path and query on `WEAVE_ORIGIN`. API, MCP, `/ingest`, `/.well-known/` and `/_supervisor/` paths, `/api/health` included, are still served on whichever name they reach, and loopback, `WEAVE_ORIGIN`'s own host and `WEAVE_MCP_ORIGINS` hosts are never redirected. Gate: `test/security/host-allowlist.test.mjs` (2 new cases).
- **The document outline rail works like the ChatGPT desktop outline** (Feature #350): the resting rail rides the vertical middle of its scroll box, its dashes share one length and the dash for the section on screen is body-coloured. Hovering the rail grows each dash to its heading depth, and the dash under the pointer, or under keyboard focus, shows a card beside the rail with the heading and a three-line preview of its section. A click jumps to the heading. The click-to-open label panel from Feature #151 and the native dash tooltips are gone. Gates: `test/doc-rail.test.mjs`, `test/doc-rail-browser.test.mjs`.
- **On a phone the selected change shows inside the Activity sheet** (Issue #978): selecting a document revision drew its diff in the record body, which sits under the full-screen sheet, so Restore was chosen blind. At 600 px and below the diff, or the field change card, now draws inside the sheet under the selected row, below Restore and the Compare picker. The desktop pane keeps drawing it in the record. Gate: `test/phone-activity-sheet-browser.test.mjs` (chromium 390x844 and WebKit iPhone 15).
- **On a phone, Create keeps the New row sheet open for the next one** (Issue #988, Net Tasks #125): Create, or Return in the Name field, used to wait for the write, close the sheet and open the new row, so a second row took four taps and a wait. On a phone the name and choices now clear at once, the cursor stays in Name with the keyboard up, the write runs in the background and a toast names the new row with an **Open** action. A refused write puts the name back and shows the reason. **All fields** still creates the row and opens it, and the desktop sheet is unchanged. Gate: `test/phone-new-row-sheet-browser.test.mjs`.
- **A phone can grab an attachments block or a related table by its grip** (Issue #886): both blocks pass `.opt-grip` as their only drag handle, and on a phone it measured 16x18, too small for a finger. Under `(pointer: coarse)` the two grips are now 44x44 targets, the phone tap-target size `.opt-row .opt-grip` and the List row grip already use. Negative margins keep the in-flow footprint at 16x18, so the 393px row spends no width, and the glyph stays where it was. The target sits above the related table's card so its lower corner still answers. Desktop grips are unchanged. Gate: `test/phone-row-grip-browser.test.mjs` (3 new cases: on the iPhone profile both grips are 44x44 targets whose four corners hit the grip, spend no more than 16x18 of the row and keep the glyph in place; a press 3px inside the target edge lifts the attachments block and drops it one place up; on desktop both grips stay glyph-sized).
- **On a phone Report a problem moves into Your menu** (Issue #983): the floating bug button sat over rows, the record and the Activity sheet on every phone screen. At 600 px and below the button is gone and Your menu (the ⋯ beside the account chip in the menu drawer) carries a 44 px Report a problem row. It closes the drawer and opens the same report panel as a sheet 12 px inside both screen edges. The desktop keeps the 26 px corner button. Tests that opened the panel from the button on a phone go through `openBugReport` in `test/lib/browser.mjs`, and the cut-off sweep opens it from Your menu. Gate: `test/bug-fab-phone-browser.test.mjs` (chromium 390x844 and WebKit iPhone 15, both themes).
- **Phone list chips get a 44 px hit area** (Issue #1032): on a phone the list row's state mark was a 65x20 target and its value chips 42x24, and a tap a few pixels right of the state opened the row. `weavePhoneRow.hitBoxes` now gives the state mark and every chip a box of at least 44x44 around the drawn pill, which keeps its size, slides it inside its row and splits every gap with a neighbour (another chip, the #id link, the name) at its midpoint, on whichever axis leaves both boxes biggest. At Standard density the name keeps its two-line slot, so the chip line sits 58 px down in every row and the state band and the chip band are 44 px each. A tap now routes by the point the finger touched down, because Chromium and WebKit move the synthesized click up to 9 px toward a nearby clickable row. Compact and wrapped rows are too short for two 44 px bands and split the gap at its midpoint. Gates: `test/phone-chip-hit-browser.test.mjs` and the `hitBoxes` cases in `test/phone-row.test.mjs`.
- **On a phone the Group popover opens as a bottom sheet** (Issue #809): the sliders sheet on a list view already lists Group, but tapping it opened the 340 px desktop popover anchored under a row inside the sheet, short of the bottom edge (651 of 659 px on WebKit iPhone 15) and boxed in beside the open toolbar. At 600 px and below `.table-group-popover` now takes the same bottom-sheet rule as menus and pickers: full width, pinned to the bottom edge, a grabber on top, the dimmed backdrop and the reduced-motion opt-out. The levels, their reorder grips, Completed by and Nest by are unchanged, and a desktop keeps the anchored popover. Gate: `test/phone-group-sheet-browser.test.mjs` (5 cases: WebKit iPhone 15 and touch Chromium in both themes find a 44 px Group row in the sliders sheet, open the levels as a full-width bottom sheet, remove a level and see the view save it; a desktop popover stays under its button).
- **On a phone, + shows on a list view right after a cold load** (Issue #1006, Net Tasks #125): a table whose view uses the list layout drew without syncing the phone bar, so + depended on whether the workspace list answered before or after the route was set. On Net's Tasks table it stayed hidden on 2 of 3 cold loads, with the bar reading "Search Net". The list branch of `drawDatabase` now syncs the bar as the grid branch does. Gate: `test/phone-new-row-sheet-browser.test.mjs`.
- **A record on a phone keeps its name in the title, not the crumb** (Issue #982): the peek crumb cut the name to "#1 Acme r..." beside four 44 px buttons, then the title repeated it in full. At 600 px and below the current record crumb now reads the table and the number, "Deal #1", and on the full record page, where the table crumb already sits before it, just "#1". The crumb's tooltip still names the record, and a record with no number keeps its name. The desktop crumb is unchanged. `rowCrumb` carries the table name for this. Gate: `test/phone-record-crumb-browser.test.mjs` (chromium 390x844 and WebKit iPhone 15).
- **A table on a phone drops its empty icon slot and tightens the gap under its title** (Issue #981): a table with no icon drew the dotted ghost ring left of its title, pushing the title off the gutter, and 32 px of margin and padding sat between the title and the first row. At 600 px and below the empty slot is hidden, so the title starts at the gutter, and the header keeps 12 px under the title. The table's ⋮ menu gains Add icon… on a phone while the table has none, the way Add description… stands in for a hidden description, so an icon can still be picked; menu-core's table settings section is now `icon`, `description`. A table with an icon still shows it as a 44 px target, and the desktop keeps the ghost ring. Gate: `test/phone-table-head-browser.test.mjs` (chromium 390x844 and WebKit iPhone 15, both themes); the cut-off sweep opens the phone icon picker from the menu.
- **The production boot path, an old database and supervisor failover run in tests** (Issue #1077): `test/fixtures/weave-v0.4.100.db` is a workspace seeded by v0.4.100's `scripts/seed.mjs` and copied with `sqlite3 .backup`. `test/serve-boot.test.mjs` spawns `node bin/weave.js serve` the way the supervisor does (IPC, `--port 0`, `WEAVE_DEFER_MIGRATIONS=1`, nightly backup armed), reads the seeded rows back, checks the store adds `doc_revisions.restored_from` to a pre-#588 copy and that boot writes wait for `migrate`, then stops it through disconnect; it also covers `weave key` set, list, reveal and delete and `weave audit` against a temporary keystore. `test/supervisor.test.mjs` adds a serving worker killed with SIGKILL outside and inside its watch, a worker that stops answering and trips the 502 tally, and a candidate whose health check answers 502. Tests only; no `src/` or `bin/` change.
- Public production status page with observed uptime for web, API, and MCP, incident timelines, and an independently hosted backup (Feature #338).
- **A query can name a saved view** (Issue #839): `weave_query {view}`, `weave query <table> --view <name>` and `POST /api/tables/:table/query {view}` take a table view by id or name and answer the rows that view shows. The engine's new `viewQuery(table, view)` is the one server-side translator: each saved filter becomes `[field, 'in', names]` (a toggle's on and off names become `true` and `false`, as the grid sends them), ANDed with the caller's `where`, and the view's sort and search apply unless the caller passes its own. An unknown view answers not-found with the views the table has. Gate: `test/query-view.test.mjs` (the translation, the merge rules, and the same fixture through MCP, CLI and REST).
- **Sorted table pages stop re-sorting the whole table on every request** (Issue #1093): `query` computed the sort key of every live row on every page, so the first page of a 50,000-row table sorted by a lookup took 81 ms (p50) and 94 ms (p95) over HTTP, and `GET /api/tables/:id` walked every entity in the workspace to count rows (17 ms). The engine now keeps the undressed sort keys, the sorted row order of an unfiltered query and the live row count per table on its row index, and drops all three on every write (`#mark`, `save`, `maybeRefresh`, a rolled-back write). The API shape and the result order are unchanged: `test/query-sort-cache.test.mjs` compares the cached and uncached answers byte for byte across sort directions, nulls, ties, deleted rows, lookups and a rollup, and after every kind of write. `scripts/bench-query.mjs --rows N` builds a scratch workspace and prints p50 and p95 for five requests plus RSS; `test/query-volume.test.mjs` keeps the 50,000-row lookup-sorted first page under its budget in the unit lane.
- **The 50,000-row query budget is a GitHub runner figure** (Issue #1093): `test/query-volume.test.mjs` shipped with a 6 ms p95 budget measured on a laptop (3.6 to 4.5 ms warm); the unit-shard readings on GitHub runners across the next five PR runs were 6.0, 6.5, 6.6, 9.1 and 9.2 ms (the two highest on PR #408, which touched no engine code), so the gate turned red on unrelated PRs. The budget is now 15 ms, the worst observed runner reading (9.2 ms) plus 50 percent rounded up, the failure message says the number is a runner figure, and the test prints its measured p50 and p95 as a diagnostic line on every run so the next budget comes from the logs rather than a laptop.
- **The reference cards fold** (Issue #993): the References and Referenced by cards in a record's side column had no control, so a long reference list could not be put away the way Activity and comments can. Each card now carries a head caret that hides its chips and keeps the `References · N` count in view. The fold is remembered per table and per card in the same `localStorage` store document sections use, so every record of that table opens the card the same way. Gate: `test/references-fold-browser.test.mjs`.
- **Reference chips in the editor draw on one line again** (Issue #991): the editor's reference overlay slot carries `wv-prose`, so its chip took the read-view `white-space: normal` and broke between the State pill and the title, doubling the slot to two lines and laying it over the text after it. `public/chip.css` now keeps a chip inside `.doc-ref-slot` on one line; a title too long for the column truncates with the slot's ellipsis. Gate: `test/doc-ref-chip-one-line-browser.test.mjs`.
- Workspace pages keep opening when the browser blocks access to local storage (Issue #956).
- Returning to weave resumes the workspace explicitly selected in this browser, including workspaces opened through a root account. Listing workspaces preserves that choice, and revoked access still falls back to an accessible workspace (Issue #957).
- **A filtered table rollup reads its child table once per query** (Issue #999): a Spaces rollup with `via` and a `where` listed and filtered the whole child table each time a row resolved it, so 1,000 rows that look up the same rollup ran 1,000 filter passes. A query now keeps one filtered child list per rollup field and every row reuses it; the list is dropped when the query returns, so the next query sees every write. A grid of 1,000 rows over a filtered 10,000-row rollup went from 45.2 s to 0.9 s. Gate: `test/rollup-shared-rows.test.mjs`.
- With the thirty-minute apex sign-in gone, a visit to the weave.grunion.ai root opens the workspace named first on the `wv_rail` cookie, on its own host and last page, instead of the sign-in step. A pending invite now keeps the start list so it can be accepted (Net #124).
- An apex row, table, space or share link now opens on the host of the workspace that holds it rather than always on Net (Issue #992).
- A signed-in visit to the weave root now opens the workspace used last, on the page it last had open, instead of stopping on the workspace list. A deep link still wins, a gone or revoked workspace falls back to the first one the sign-in opens, and the list appears only for a sign-in with no workspaces, from the in-app Find my workspaces item, or after Use another account and Add login, which return to `/start?pick=1` so the second login's workspaces show beside the first (Issue #977). This re-lands PR #293, which the sheriff reverted in PR #314 because four browser tests still opened `/start` signed in and expected the list.
- Additional workspace naming starts blank instead of reusing an occupied account address. (Issue #1014).
- Setup drafts survive refresh within the same signed-in browser session; interrupted creation never automatically replays. (Issue #1009).
- **The setup page art keeps the mark at one size in any window** (Issue #990): the stage beside the sign-in pane drew an 816 x 528 landscape art with `object-fit: cover`, so a narrow or portrait window (900 x 1000, an iPad held upright) scaled it about 4x and showed two strands of the mark at poster size. With `object-fit: none`, the stage draws a 3840 x 2160 art at its own scale, so the mark stays 320 px wide whatever the window and rings reach every edge of a stage up to 4K. `brand/build-radiate.mjs` takes a canvas size and writes both the half-letter marketing pair and the `public/brand/weave-radiate-{light,dark}.svg` stage pair; it also carries PR #200's half-letter asset, which this change supersedes. Gates: `test/brand-radiate.test.mjs` (4K canvas, mark scale, edge fill, ring pitch, public copies match the generator) and `test/start-stage-browser.test.mjs` (stage at 900 x 1000, 1024 x 1366, 1440 x 900 and 2560 x 1440; single pane at 800 wide).
- **Share links and workspaces get generated isolation suites** (Issue #1079): `test/security/share-matrix.test.mjs` reads the mode rungs from `src/shares.js` and asserts every mode (read, comment, edit, manage) on every scope kind (entity, view, table, space, workspace) against 15 share-door probes (page GETs, a page POST, the scoped API reads, a comment, PATCH, POST and DELETE of a row), 300 cells, with the refusal text checked and every refused write leaving the rows and audit log unchanged. Expired, revoked and private grants get a row each, and the shared table page is held to rendering a many-relation cell as names joined by ", " and a one-relation cell by its name. `test/security/cross-workspace.test.mjs` derives every resource route from the dispatcher and sends each, under all five methods, with workspace A's ids from workspace B's base path with B's editor token and B's session: every probe is refused (401, 403 or 404), none carries A's data and A's audit log does not grow; A's share token and account token open nothing under B, and B's credentials open nothing under A. No leak was found. Tests only, no `src/` change.
- The phone sheet grabber reads its dismissal distance from the release event, so a drag past a third of the sheet closes it even when the finger's last movement arrives with the pointerup instead of as a pointermove. Under load the final move could coalesce or land after the release, leaving the measured distance short, and the sheet sprang back. (Issue #1064)
- **The sign-in guide covers the account chip and per-workspace sign-out** (Issue #718): the Door C Handbook guide described sessions only from `/auth`, and never said where to see who you are signed in as or what the sidebar menu's two sign-out rows do. A new section, Your account and signing out, covers the identity panel at the sidebar foot, **Sign out** (ends this workspace only) and **Sign out everywhere** (one `POST /api/auth/logout?everywhere=1`), signing out of a workspace opened through the hub root (the root stays signed in, Issue #716), and the 30-day idle rule applying per workspace. Gate: `test/deploy-artifacts.test.mjs` (1 new case: the guide names every menu label the app still uses and the section covers the idle rule, the hub-root mark and the single call).
- **Sign out everywhere sends one request** (Issue #719): the account menu's Sign out everywhere posted `/api/auth/logout` once per workspace in the rail under `Promise.allSettled`, so a slow or refusing workspace left the sweep half done while the page still landed on the signed-out sign-in page, and the per-workspace calls never cleared the start cookie or the rail. It now posts `/api/auth/logout?everywhere=1` once (Feature #280), which ends every session the browser holds and clears the start cookie and the rail in one response. The per-workspace loop runs only when that call answers 404, for a server older than Feature #280. Gate: `test/account-chip-browser.test.mjs` (the recorder now keeps the query string; 1 rewritten case asserts exactly one `?everywhere=1` call, 1 new case asserts the 404 fallback posts each workspace's logout and still lands on the sign-in page).
- **Signing back in no longer shows the setup donut** (Issue #987): a signed-out visitor at the weave root lands on `/start`, whose sign-in pane is also setup step one, so a returning person saw "Step 1 of 4 · Account" at 25% and read it as being sent back through setup. `progress()` in `public/setup-core.js` now returns nothing for the account step while the visitor is signed out, so the sign-in and create-account forms show no donut. The donut first appears on the workspace step at "Step 2 of 4", half full. Gates: `test/setup-core.test.mjs` and `test/setup-flows-browser.test.mjs`.
- Explicit signup intent survives the legacy auth redirect and opens the signup pane. (Issue #911).
- Empty and general setups appear before the template catalog. (Issue #1012).
- The workspace picker shows loading feedback immediately and names the workspace being opened (Issue #958).
- **A start page tile no longer flickers under a resting pointer** (Issue #1001): hovering an agent tile on `/start` lifted it one pixel, which moved it out from under a pointer resting on its bottom edge, dropped it back, and repeated every frame. The hover now changes the border colour only. Gate: `test/start-tile-hover.test.mjs`.
- **The supervisor streams a release download to disk** (Issue #954): `unpackCommit` read the whole codeload tarball into memory (19 MB, copied twice) and `unpackRelease` gunzipped it into one 44 MB buffer, so the idle production supervisor kept about 120 MB after each swap (166 MB private memory against about 50 MB for a bare Node process). The fetch body now runs through `zlib.createGunzip()` into `streamTar` (`src/backup.js`, sharing `readTar`'s header parsing), which writes each entry to the `.part` directory as it arrives. The path-escape refusal still runs per entry, and a truncated or corrupt archive is refused. On a 36 MB fixture one unpack now adds about 7 MB RSS instead of 127 MB. Gate: `test/supervisor.test.mjs`.
- **The System space page says why it offers no space controls** (Issue #849): `showSpace` passed `actions: space.system ? [] : [...]`, so the registry space drew an empty header toolbar: no Share, no Use template and no ⋮. A reader who reached that page by URL or by search found nothing to act on and no reason given, which is what the report behind this row asked about. The header now carries one line where the controls would be, `Weave owns this registry space, so it cannot be shared, used as a template or deleted.`, in secondary type. The controls stay absent, deletion is still refused in the engine, and a user space keeps Share, Use template and its ⋮ menu. Gate: `test/system-space-menu-browser.test.mjs` (4 new cases, both themes: the note is visible, readable against the page and names sharing, templates and deletion; the three controls stay gone; a user space carries no note and keeps Share).
- **A view saved as Table stays Table** (Issue #740): the engine used to delete `layout` when it was set to `table`, so a view someone switched to Table read back exactly like one nobody had touched, and a phone could never tell them apart. `layout` now has three states: `list`, `table`, and unset. `weave_table_view`, `weave table view --layout`, REST and the System/Views `Layout` cell all keep `table`; `null` (or an empty `Layout` cell) clears the choice, and Reset view clears it too. Gate: `test/list-layout.test.mjs` (a Table choice reads back as `table`, survives `describeSchema`, and `null` clears it).
- **A table query reads its own table, not the whole workspace** (Issue #998): the engine keeps a per-table row map beside `state.entities`, updated on create, the create rollback and hard delete, and rebuilt whenever the state object is replaced (load, refresh, import, rollback). `listEntities`, the query path, permalink and `#id` lookups, the registry row lookup and the workflow row list read it. `#queryIn` walks the table once and derives the live count, `countAll` and `trashCount` from that one pass. A 100-row query in a 50,000-row workspace went from a 109.8 ms median (336.3 ms p95) to 3.6 ms (17.2 ms p95) on the same machine. Gate: `test/engine-table-index.test.mjs` (no whole-workspace enumeration, fewer than 200 rows read, a 30 ms p95 budget, and the map following create, trash, restore, purge, undo, failed create and import).
- **`npm test` fails in seconds when the Playwright browser is missing** (Issue #557): a worktree whose Playwright resolved but whose browser executable was not installed ran the whole unit lane, printed its green `# pass` block, then failed every browser file at launch and, on 2026-09-29, never exited. `scripts/test.mjs` now launches the browser once before either lane when the selection holds a browser suite; when the launch fails it prints `# BROWSER MISSING: <the launch error>` and the `npx playwright install <browser>` line, and exits 1 without starting a lane. Every run now ends with one `# RESULT green|red: unit lane ..., browser lane ...` line, so a first-lane summary can no longer be read as the verdict. Gate: `test/test-runner.test.mjs` (4 new cases, including an end-to-end `npm test` against an empty `PLAYWRIGHT_BROWSERS_PATH` that must exit 1 within 60 s).
- **Undo after moving a table, space or share page to the trash restores it in place** (Issue #856): `trashRow` reopened the object on Undo whenever it had been open at delete time, so a reader who had since moved to another page was pulled back to it. The trash row now records the page the delete left the reader on. Undo reopens the object only when the reader is still on that page; otherwise it restores, leaves the current page alone and shows `Restored` with an Open action. Gate: `test/menu-trash-browser.test.mjs` (3 new cases: a table, a space and a share page trashed while open, then the reader opens another table; Undo keeps that table's hash and Open in the toast goes to the restored object).
- Hosted New workspace opens the shared setup flow instead of root-only workspace administration. (Issue #1007).
- **A workspace link signs in on the setup page** (Issue #994): with `WEAVE_BASE_DOMAIN` set, a signed-out visit to a path-mounted workspace link such as `/w/weave/e/1` used to go from `/w/weave/auth` straight to the identity provider, which showed Clerk's plain hosted sign-in page, and its Sign up link skipped onboarding. The workspace `/auth` now answers `302 /start?next=<workspace path>`; `/start` keeps a `next` only when it is a same-origin `/w/<workspace>/` path. The in-pane Clerk sign-in then goes on through that workspace's sign-in and lands on the linked page, a sign-up goes to the setup steps, and a visitor already signed in on `/start` who opens that workspace goes straight on. `/w/<workspace>/auth?signed-out=1`, workspace hosts, invite links and deployments without a base domain keep their sign-in as before. Gates: `test/setup-routes.test.mjs` and `test/workspace-signin-start-browser.test.mjs`.
## v0.4.107 (2026-10-17)
- **The Activity page loads one page, not the whole log** (Issue #850): `showActivity` asked `GET /api/activity` with no `limit`, and the route's `boundedLimit` falls back to `null`, so the view received every event the workspace had ever recorded and painted a `tr` for each one. On the weave workspace that is 26,067 rows and a 13.7 MB answer, and the hop from a table to `#/activity` took 10.0, 10.1, 10.4 and 11.0 seconds against 283 to 387 ms for a 1,300-row Issue table. `ACTIVITY_PAGE` (500) now rides on the request, the same page size the record panel already asked for, and the note says `Showing the newest 500.` whenever the feed holds more. The same four hops on the same database now take 161, 318, 316 and 298 ms with 500 rows painted. `activityFeed` keeps its `limit: null` meaning for the engine, the CLI and MCP, so no verb changed. Gates: `test/activity-page-size.test.mjs` (5 cases), `test/activity-page-size-browser.test.mjs` (4 cases, both themes).
- **Actor chips ask Users for the handles they draw** (Issue #850): `workspaceUsers()` read the whole Users table with `POST /tables/<users>/query {}` and cached it, which put a full table read behind the first Created By cell, record panel chip or Activity row on every page load. `actorChipEl` now names the one handle it needs, `workspaceUsers` collects the handles a paint asks for and `flushUsers` sends them as one `where: [['Name', 'in', want]]` query per microtask. Opening `#/activity` on the weave workspace sends one Users query naming four handles instead of a bare whole-table read. Grid, record panel and Activity chips resolve as before, in both themes. Gate: `test/activity-page-size-browser.test.mjs`, `test/system-fields-people.test.mjs`.
## v0.4.106 (2026-10-17)
- The doc-kinds browser waits carry a draw budget and report page state on give-up, so a slow mermaid load no longer needs the gate retry (Issue #364).
- **One meta tag style names a field type** (Issue #773): `.term-tag` set the monospace face and `.wv-tag` set none at all, so the same 11px secondary type word came out mono in the field editor tray and sans on every other surface. `.wv-tag` now declares the face once, `font-family: var(--tblr-font-sans-serif)` with `font-size: var(--fs-meta)` and `color: var(--tblr-secondary)`, and `.term-tag` is gone: the plural row builds a `.wv-tag` like everywhere else and keeps its 44px right-aligned column through `.term-plural-row .wv-tag`, a parent rule, so layout is the parent's business and the face is the tag's. Sans won on call sites, since the row's own fix shape kept the `.wv-tag` name and asked `term-tag` to give up only its font; it is also the face the surrounding field names and labels already use, and the type word reads as a label rather than as code. The row measured 19 `.wv-tag` uses against 9 `.term-tag` at v0.4.88; Issue #775 has since taken the computed type words off record field rows, so the live counts are 4 against 1, and the two faces were the only thing left to settle. `tools/ui-catalog` drops its now-dead `term-tag` entry. Gate: `test/ui-contract.test.mjs` asserts `.wv-tag` names the sans face at `--fs-meta` in secondary, declares no layout of its own, that no `.term-tag` rule or call site survives, and that the 44px right-aligned column comes from the parent rule.
- **Collapsing the nav no longer hides Activity, Workflows, Users and Trash** (Issue #909): `#app.nav-collapsed #sidebar { display: none }` removed the sidebar subtree, and the four pinned routes lived inside it, so a collapsed nav left only workspace switching and reaching Trash meant reopening the nav first. The 52px rail now carries the same four routes as icon buttons below the workspace avatars, behind a hairline, each with a tooltip label and an `aria-label`, the current one drawn as a filled `.ws-icon.active` tile carrying `aria-current="page"`. `#rail-routes` is a static slot in `public/index.html` that `renderNav` fills from the same `registryTable('workflows')` and `registryTable('users')` reads the sidebar's System group uses, so the two never disagree about which route is current. CSS shows the slot only under `#app.nav-collapsed:not(.nav-peek)`, which keeps exactly one visible copy of each route in every state: the sidebar group when the nav is open or peeking, the rail when it is collapsed. Labels under the icons stay out; that is the width decision the row left open. Gate: `test/ws-rail-routes-browser.test.mjs` (3 cases: every route visible and one click away in the collapsed rail, a divider above the group with the clicked route filled and the only one carrying `aria-current`, and no route visible twice with the nav open).
## v0.4.105 (2026-10-17)
- Marketing kit in `brand/kit/`: the weave badge (DATA · AGENTS / SYSTEMS) in 15 treatments, release and footer badges, link-preview, square, banner and story posters, and contour, rope and dot patterns grown from the mark, each as SVG and PNG. `manifest.json` names every asset's code, size, ground and use, the README carries recipes for site, email, social and print, and `test/brand-kit.test.mjs` keeps the files in step with `build-kit.mjs`. (Feature #347)
- **Reversing a scroll costs nothing** (Issue #938): the row window kept one buffer of drawn rows behind the reader and two ahead, picking the deep side from whichever way the reader was going, so a reversal mid-fling spent that one buffer and reached rows the window had dropped. It now holds two buffers on both sides and reads no direction at all: `windowFor` loses its `direction` argument, `travelFor` and the `win.dir`/`win.lastTop` state go with it, and arriving at a row from above draws the same window as arriving from below. PR #269 held the same shape and was reverted because the cut-off gate counted the extra rows parked below the fold; Issue #937 taught that probe to read only what the viewport shows, so the gate no longer moves with the window. Measured against `origin/main` on the same machine, every cut-off count is identical. Gates: `test/grid-window.test.mjs` (two cases: two buffers on both sides, and a stray `direction` argument moves nothing), `test/grid-window-feed-browser.test.mjs` (1 new case: the drawn rows reach as far behind the reader as ahead, whichever way the reader came; red on the old window at rows 354-428 against 374-448).
- **The new-row sheet gesture case stops needing a retry** (Issue #903): `test/phone-new-row-sheet-browser.test.mjs` measured the grabber while the sheet was still springing back from the short drag, so the next drag reached for a point the grabber had already left, and on a loaded hosted runner the long drag missed the grabber and the sheet never dismissed. Measured here in WebKit on an iPhone 15 profile: at pointerup the grabber sits 39px below its resting position with one animation still running, while the sheet's inline `style.transform` already reads empty, which is why the case's own "leaves no transform behind" check could not see the motion. `dragSheet` waits for `atRest` before it measures, the sheet's open animation is awaited before the first grabber measurement, and the spring-back is now asserted at rest through a computed identity transform. The dismissal and no-write assertions are unchanged. Ten consecutive runs under `WEAVE_CPU_THROTTLE=4` on macOS: `# pass 15 / # fail 0 / # skipped 0`, no retry. Gate: `test/phone-new-row-sheet-browser.test.mjs`.
- Consent-gated in-app usage events: when `POSTHOG_KEY` is set, the app shell loads the same cookie banner as /start, adds a Cookies row to the personal menu, identifies a signed-in person by the Clerk user id (on /auth too), and after Accept records `app_opened` once a day plus the named actions `workspace_created`, `table_created`, `row_created`, `rows_duplicated`, `rows_imported`, `template_used`, `invite_sent`, `invite_accepted` and `agent_token_created`, with fixed values and counts only. The privacy policy names them. (Feature #337)
- **The rail-inset gate now measures something on every machine** (Issue #636): `test/ws-rail-inset-browser.test.mjs` ended its measurement with `t.skip` whenever the page reserved no scrollbar gutter, so on this Mac it asserted nothing and a clean suite could never report `skipped 0`. The gutter is unreachable rather than merely absent under macOS overlay scrollbars: `public/style.css:88` gives `html` and `body` `overflow: hidden` while `#app` is mounted, so weave scrolls `#main` and never the document, and the skip fired on every platform. The case asserts the rail at left 0, the sidebar at 52, `scrollLeft` 0 and no horizontal page overflow across all four views with no condition on it, and a second case appends a child taller than the viewport and asserts the page still refuses to scroll, reserves no gutter and carries no `:root` margin. Both go red when `:root { margin-left: 0 }` becomes 16px, at rail 16 and sidebar 68, the numbers Issue #546 recorded before its fix. Gate: `test/ws-rail-inset-browser.test.mjs` (2 cases, `# pass 2 / # fail 0 / # skipped 0` in the browser lane on macOS).
- A workflow state chip now paints the colour its state names. Eight `.k-state.cat-*` tone rules in `public/chip.css` outranked `.hue-*`, so an In progress state coloured green still showed blue in the grid, pickers, search, relation chips, cards and the field dialog. The rules are gone; `.hue-*` is the one source of chip colour, the hue-less filter chip takes its category hue, and the phone applet builds all ten hues from `chip.css` instead of a five-hue copy whose dark values had drifted. A paint contract test (every chip kind, hue and category, both themes, Chromium and WebKit) runs on every pull request, a surface test compares painted colours across eight surfaces, and a CSS lint refuses any other rule that sets `--fill`, `--text` or `--dot`. (Issue #969, Issue #771)
## v0.4.104 (2026-10-16)
- Each agent on the connect step shows its own logo: Claude, ChatGPT, Gemini, Claude Code, Codex, Antigravity and Gemini CLI. (Issue #958)
- Every sign-up at weave.grunion.ai starts onboarding. With `WEAVE_BASE_DOMAIN` set, the apex `/auth` goes to `/start`, and any sign-in begun on the apex returns to `/start`. A new person, including one arriving from a landing page link or from Clerk's hosted page, lands on the setup steps instead of a No access page. Workspace hosts keep their own sign-in (Feature #324, Issue #910).
- The setup page, its weave tile, the sign-in card and the 404 page draw the brand rope. They each drew their own mark, and the setup page's read as three closed loops; all four now take the masked h3 mark from `src/mark.js`, inline, in both themes. (Issue #963)
- Clerk's security emails (new device sign-in, account locked, two-step verification, passkey and password changes, email address change, codes and email links) use weave's email layout and lockup instead of Clerk's stock template. The templates live in `src/mail.js` beside the invite emails, and `node scripts/clerk-emails.mjs --push` sends them to the Clerk instance. (Feature #343)
- **Click budget for every journey** (Feature #338): the human-simulating journeys now count the clicks each action takes and log them on every PR. `test/click-budget.json` holds the budget per journey and cell; budgets only ratchet down, and a change that adds a click fails unless it raises the budget with a written reason under `raised`.
- The cut-off text gate reads only what the viewport shows, so a grid's off-screen buffer rows no longer decide the baseline counts ([[Issue#937]]).
- Scrolling down a long table stops at the rows that are loaded, on a **Loading more** row that runs the weave loader, and the next page is asked for three screens before the reader gets there. A fast flick no longer lands on blank grey rows. Jumping to a row that is not loaded yet (End, a new row) loads every page up to it first (Issue #962).
- Scrolling back up a long table never redraws a row you already saw. A refresh used to keep only the pages under the window, so rows above came back blank and waited on a fetch; it now re-reads every loaded row in batches of 400 and swaps them in at once (Issue #961).
- The loading animation plays again on hosted workspaces. The sign-in wall had been answering the rope fragment `/brand/weave-loader-rope.html` with a redirect, so the loader sat on its still mark; brand fragments now pass the wall like the other brand assets, and a grid loader that opened before the rope landed picks it up when it does (Issue #960).
- weave.grunion.ai opens on sign-in, with Create an account beside it. A first sign-in through Google or another single sign-on creates the account on the spot. Sign-in can no longer be skipped from the setup page; Skip setup stays on the later steps, and `/start?setup=skip` still goes straight to an empty workspace after sign-in. A link with a page hash, such as /#/settings, no longer sends a signed-out visitor to Clerk's hosted page (Feature #327, Issue #933).
- One browser test walks onboarding end to end: sign-up, setup, the Claude connector's sign-in in a new tab, the first tools/list and tool call, then back to a setup tab that turns connected without a reload. It runs on desktop and on the iPhone profile and pins the counts in `test/onboarding-steps.json`: 3 pages, 1 sign-in, 1 consent screen and 9 clicks. The counts may only go down. (Feature #335)
- Consent-gated PostHog onboarding funnel: when `POSTHOG_KEY` is set, /start and /auth show a cookie banner, and only after Accept does weave load PostHog through the same-origin `/ingest` proxy and record each setup step, the sign-up, workspace, template, agent and completion milestones, identified by the Clerk user id alone. Decline, or no key, sends nothing. The privacy policy names PostHog and the shared `grunion_consent` cookie. (Feature #336)
- The AI app step on the setup page says up front that connecting opens a weave sign-in tab and to come back after Allow. Once the app connects, the setup tab retitles itself with the app's name, so it stands out among the open tabs, and fires a notification when notifications are already allowed. (Issue #936)
- The "Your workspaces" page names the account you are signed in with, and each workspace is a card with its logo (or letter tile), its address, your role and an open arrow, the one you opened last on top. "Add another sign-in" reads "Use another account", and every target on a phone is 44px tall. `GET /api/start` rows carry `id`, `host`, `role`, `logo` and `seenAt`, and `GET /api/start/logo/<workspace>` serves a logo to a sign-in that opens that workspace. (Issue #964)
## v0.4.103 (2026-10-16)
- The Billing page says who holds which seat. Under each name the seats list prints that person's verified sign-in address, so a team workspace is readable when account names do not match the people behind them. `GET /api/billing` carries the address only for an architect, the same gate Members uses; every other member reads the seats as before (Issue #921).
- The setup page's art now fills its whole panel at every window size. The form column scrolls inside the page, so the art never ends early and leaves a gray band below it, and a phone still hides it. The connect step reads shorter: each tile says in one line what it takes or which plans can connect, the steps run a dozen words or fewer, the page-wide plan note is gone, and the step's links (home, Privacy, Terms) are checked by a test. (Issue #954)
- Every MCP tool now carries annotations: a title plus `readOnlyHint`, `destructiveHint` and `openWorldHint`, so clients and connector directories can tell reads from deletes. `/support` and `/docs` (agent setup, with every tool and its marks) answer without sign-in, and `/.well-known/openai-apps-challenge` serves the token in `WEAVE_OPENAI_APPS_CHALLENGE` as plain text, or a 404 when it is unset. The default `tools/list` grows from 15,389 to 17,218 bytes (Feature #331).
- The left nav's scroll bar runs only beside the space list. The list scrolls inside the sidebar between the search head and the pinned foot, its bar sits at the panel edge, and the head and foot no longer ride sticky offsets, in the sidebar, the hover peek and the phone drawer alike. (Issue #957)
- Safari draws the same thin scroll bar as Chrome. Tabler's 16px WebKit bar, which Safari kept on every pane while Chrome ignored it, gives way to one weave spec: `scrollbar-width: thin` for Chromium and Firefox, and an 8px WebKit bar with a 2px inset thumb at Tabler's 20% tint, 40% on hover. The nav, the main panel, the dock and every doc table get their 8px back. (Issue #956)
## v0.4.102 (2026-10-16)
- The personal menu's keyboard test now expects the Settings row that Features #295 and #297 added, so main is green again. (Issue #931)
- The bug panel's submit button now says **Report issue** instead of Send, and carries `⌘↵` inside the button rather than as a grey chip beside it, so the shortcut is visible where the action is. A filed report turns the same button into **Reported**. (Issue #847)
- The connect page lists Claude, ChatGPT and Gemini under Chat apps, and Claude Code, Codex, Antigravity, Gemini CLI and Something else under Harnesses. Cursor and VS Code are gone, along with their install links. ChatGPT and Gemini say which plans and accounts can add a custom MCP server. (Issue #950)
- The tab shows the weave mark in Safari: the server answers `/favicon.ico` with the built icon and serves every `.ico` as `image/x-icon`, and the setup, billing and not-found pages name the ICO beside the SVG icon the way the app shell does. (Issue #848)
- The stacked-rhombi `layers` icon is retired everywhere (Issue #948). The Projects and tasks category and the Team projects template's Projects table show `folder-kanban`, the table Group button shows `group`, the rollup command shows `sigma`, a deck's promote button shows `copy-plus` and a workspace search hit shows `house`. The icon picker no longer offers `layers`, and a value stored as `lucide:layers` draws as `lucide:folder-kanban`.
- Permanent delete moved to a new Settings page, opened from the personal menu (the ⋯ beside your name at the foot of the sidebar). Each trashed row or share page there has a Delete forever button; you type the item's name to arm the hold, then hold it until it fills. The Trash page now only restores. Workspace delete is back in the same place, behind the same typed name and hold, and moves the workspace to the trash, where Restore brings it back. Only a caller the server lets purge (an editor or architect) sees Delete forever, and only an architect on the hub root sees workspace delete: `GET /api/trash` now returns `purge`, and the `deletable` field on `GET /api/workspaces` now depends on the caller's rights. (Feature #295, Feature #297)
- Settings opens from the personal menu (the ⋯ beside your name at the foot of the sidebar), between Keyboard shortcuts and Sign out. The System group keeps only Workflows, Users, Activity and Trash, as Feature #333 rules (Issue #929).
- The setup picker's preview is small now (Issue #949). A pick shows one line on who the template is for, a line saying what it is like (Team projects is "Like Asana"), and a map of its tables with their icons and a line for each relation. The field lists and the table and sample-row counts are gone, and the preview stays whole above the Start button on desktop and on a 375 px phone. Search also matches the like line, so "asana" or "jira" finds its template.
## v0.4.101 (2026-10-16)
- The AI app step on `/start` lists the agents most people can finish setting up, from 28 Haiku runs: Claude, Claude Code, Cursor, VS Code, Gemini CLI, and Something else. ChatGPT, the Gemini app and Perplexity are cut because custom connectors there need a paid business plan, an admin or a region most accounts lack, and the page says so. Every agent but Claude copies one tested setup prompt carrying the workspace address; Claude keeps its click steps, and Cursor and VS Code keep their Add buttons. The step heading is "Your own agent". (#330)
- The billing page loads Clerk's UI bundle beside clerk-js 6 and passes it to `Clerk.load`, so the pricing table mounts instead of failing with "Clerk was not loaded with Ui components". (Issue #922)
- A conditional write can now carry both halves of a close: `operation: "close"` appends evidence through `field`/`markdown` and moves the workflow field named by `stateField` in one transaction, after one revision check, so an agent closing a row no longer leaves a window a person's edit can land in and lose (Issue #902).
- The dev supervisor reads main's head through the git protocol and downloads commits from codeload, so an exhausted unauthenticated GitHub REST quota no longer fails the dev swap and skips the soak. A rate-limited REST call is retried and its error names the reset time, and the dev workflow waits for dev to report the commit instead of failing on one unanswered check (Issue #930).
- Both reference cases in the slash-commands suite now wait for the chip their assertion reads, through one `docHtmlWith(page, id, pattern)` helper that polls `doc.html` for that pattern and names it with the stored markdown when the wait runs out, so the GET no longer races the save and passes on a page whose inline script merely mentions the word (Issue #457).
- A workspace made at `/start` opens on a free trial: for `WEAVE_TRIAL_DAYS` days (14 by default, `0` for none) every Editor and Architect creates and edits, and the AI app works over OAuth MCP and `wv_` tokens. After it, a paid seat is needed again and every row stays. `GET /api/billing` and the `/api/start` workspace rows carry `trial: { endsAt, daysLeft }`, and the sidebar links to Billing over the last 3 days. (Feature #329, Issue #918)
- The setup page's agent step reports an AI app as connected only after weave answered one of its MCP calls, never a call it refused. (Issue #915)
- An Architect without a seat keeps an AI app that reads: `/mcp` answers `initialize`, `tools/list` and the read tools, and write tools return a tool error with the seat-required words and the billing address. (Issue #916)
- The seat-required refusal is worded for the reader's role, and the app shows it as one toast with an Open billing action instead of one toast per refused write. (Issue #917)
- `GET /api/start` names the workspace waiting on a plan as `waiting` beside `canCreate`, the `plan-required` refusal carries that workspace's billing address, and `/api/start/open/<name>?to=billing` lands on that workspace's billing page. (Issue #920)
- The relation picker's browse list (a scoped search with no text) no longer offers rows of a table, or of a space, that sits in the trash; it now applies the same container check as text search. The lifecycle pack gains a second ring that runs delete, restore and purge for rows, tables, spaces and workspaces through a real `.db` close and reopen, and covers restore against a name a live table, space or workspace has taken, undo next to structural trash, search delisting under a trashed container, and CLI and server writes meeting through the per-request refresh (Feature #159).
- A lookup or a rollup repoints in place. Its settings tray offers the Relation, Target field and Aggregate pickers a create offers; save, and the column keeps its name, description, width, views and position while every cell reads the new recipe. `updateField`, `PATCH /api/tables/:id/fields/:id`, `weave field update --config` and `weave_update_field` take `relationField`, `targetField` and `aggregate`, checked by the same recipe check `addField` runs, so an edit is refused exactly where a create is. A new relation keeps the old target field only when its far table has it. The tray warns when the result changes kind, the answer lists under `formulas` every formula of the table that now fails on the column, and the Activity entry names the old and the new recipe so Undo puts it back. A space rollup keeps its recipe read-only. The formula scan now counts a NaN result as an error. (Feature #241)
- The AI app step on `/start` opens with weave's position: weave is AI native and adds no agent of its own, so you connect the AI app you already use. App tiles carry tinted icons from `agent-connect-core.js` data; the chosen app gets a header with Change app, the steps a numbered sequence with the copy action on the step that needs it, a status that animates while waiting and turns green with a check on connect, and the first message is the payoff. One Skip for now link. (#326)
- With a Clerk publishable key, the Account step on `/start` mounts Clerk's sign-up form in the left pane, with an "Already have an account? Sign in" switch to Clerk's sign-in form. Clerk's UI bundle (`@clerk/ui@1`) now loads beside clerk-js 6 and is passed to `Clerk.load`, which fixes the fallback to the plain button on production (Issue #922). Without Clerk's scripts the provider button stays. (#327)
- The setup page at `/start` keeps the radiating-rings art on the right for every step and shows progress as a donut in the left pane: 25% on Account, then 50, 75 and 100, with the workspace build carrying it from 75 toward 100. (#324)
- The kind-of-work step is a gallery named "Your work": a search field that filters as you type (every word, ignoring case and accents), category chips and shelves, cards, and a details panel under the picked card listing the tables it creates, their key fields and the sample-row count. Empty and Basic stay pinned at the end and are all that shows when nothing matches; Empty takes an optional description for the AI app. Arrow keys move between cards, Enter picks and Escape clears. The primary button stays pinned at the bottom of the pane and names the pick. (#324)
- A workspace that still needs a plan shows on `/start` before the next one is named, with a link to its billing page, and the last step shows the days left in a trial. (#324)
- On a phone, focusing the setup gallery's search field scrolls it to the top of the screen, so the matches show above the keyboard, and the page stays put once focus leaves the field. (#324)
- The eye on a relation subtable shows and hides the target's fields and filters the linked rows with the table filter's chips. The setting lives on the relation field as `config.subtable` (`fields`, `filter` as `where` triples, `sort`), so one edit narrows that subtable on every record, and a filtered subtable counts `shown / total` in its header (Feature #214).
- The starter catalog grows to 40 templates in 12 categories for the setup gallery: `WeaveStarters.CATEGORIES` lists each category with a label, icon and blurb, and every template carries a `category` and two or three sentences of `details`. The 15 earlier starters keep their ids and tables. (Feature #328)
- The catalog adds `basic`, one plain Items table under a new Start simple category that the gallery search falls back to, and `WeaveStarters.searchText(template)` returns the lower-case words a search should match: title, kind of work, blurb, details, category and table names. (Feature #328)
- Automations take an `upsert-row` action: it creates or updates a row of a target table matched on a key field holding an external id, with a field mapping from source paths (`row.<Field>`, `event.type`) to target fields, so a repeated fire never makes a duplicate (Feature #309).
## v0.4.100 (2026-10-15)
- The Handbook documents in-place account role changes, the last-architect guard and immediate permission updates for cached provider identities. (#896)
- The Activity panel's revert button and row icon read "Clear value" when a field was empty before the change, instead of "Revert to null", and an empty prior value reads as a dash whether it was stored as null, an empty string or an empty list (#908).
- The record Activity panel is a pane docked to the right edge of the record view: the view's full height, square edges, one left border and no shadow. The record body and its header narrow beside it and scroll while the pane stays put, on the full page and in the peek; phones keep the full-screen sheet (#906).
- A table whose row holds a long relation cell opens without an 11 s stall: the Activity pane's layout rules select from the pane forward instead of through `:has()` on the view (Issue #920).
- The Activity panel wraps a long field value inside its row instead of cutting it at the panel edge, keeps its filter chips on one line and puts the document chips that do not fit under a More menu. The cut-off sweep and gate walk a new Activity-panel journey on desktop and phone (#907).
- The Environment reference row for `WEAVE_APPLET_PASSCODE` now describes the gate the applet runs: eight wrong answers inside ten minutes refuse every later attempt with `429`, the right passcode among them, and an unset passcode leaves no applet at `/t` instead of opening one to anyone (Issue #814).
- **No em dashes in release notes** (Issue #722): CHANGELOG.md body prose carried 156 U+2014 characters that the house prose rule bans, and the GitHub Release body copies them. Each one is rewritten with the comma, colon, parentheses or hyphen its own sentence wants, read line by line. `test/changelog-fragments.test.mjs` now pins the whole file instead of the version headings, and `fragmentGuard` refuses a commit that adds or edits a `changelog.d/` fragment carrying U+2014, so the character cannot come back through the door that let it in. `changelogGuard` lets a CHANGELOG.md line be rewritten to drop the character without a version bump, and nothing else.
- Billing through Clerk Billing, off until `WEAVE_CLERK_SECRET_KEY`, `WEAVE_BILLING_SOLO_PLAN` and `WEAVE_BILLING_TEAM_PLAN` are set. One Editor or Architect is covered by the solo user plan; more than one need the per-seat team plan, and weave mirrors exactly the Editors and Architects into the workspace's Clerk organization so Observers are never billed. A paid human without a seat works as an Observer and gets a 402 `seat-required` answer on a write; a failed card never cuts access. Workspaces created before `WEAVE_BILLING_SINCE` are never billed. New `/billing` page and `GET /api/billing`, `POST /api/billing/team` (Feature #256).
- With `WEAVE_CLERK_PUBLISHABLE_KEY` set for the same Clerk instance as the sign-in issuer, the setup page mounts Clerk's sign-up and sign-in form in its left pane, in weave's colours, so creating an account no longer leaves the page. If Clerk's script does not load, the page falls back to the provider button (Feature #327, Issue #910).
- Conditional document appends and state changes reject stale row revisions under the database write lock. Authenticated clients can read a minimal full serving-commit proof (Issue #902).
- The date picker steps sideways: ‹ and › replace the ↑ and ↓ buttons for the month, year and decade steps, beside the month and year dropdowns the picker keeps. The year and decade steps gain aria-labels, and the date and range pickers name the field they edit. (#801)
- An image embed with an empty source is no longer stored on a document, by any write path, so a failed paste cannot leave a broken image behind; an embed written as an example inside a code fence or inline code is left alone. An image uploaded into a document now carries empty alt text instead of the file name, which keeps a browser-synthesised name such as a page title out of the document. (#840)
- `test/flicker-fixed.json` refuses a fingerprint the flicker gate never reads. The gate walks each journey with the DOM probe alone, so a `frame`, `jank` or `rerender` entry sat there unchecked; `fixedFault` now names the kind, the kinds the gate does read and the `frameFlashes` case that holds a frame finding instead (Issue #702).
- Serving-commit proof recognizes supervised commit tarballs that do not include Git metadata (Issue #902).
- The nav identity panel's browser suite signs in with the per-workspace session cookie, so it no longer reads as signed out after the legacy `wv_session` cookie stopped opening workspaces (Issue #717).
- Renaming an account carries its credentials with it: the keystore owner and every share naming the old account name move to the new one, so the owner and the accounts a credential was granted to keep their access. (#884)
- The Users table's Last Seen moves while someone stays signed in: a renewed session updates it, at most once an hour per account, so it shows recent activity instead of the last sign-in. (#877)
- The plain `wv_session` cookie from before per-workspace sessions is no longer read: it opens nothing and is cleared with `Max-Age=0` on the next response. Sign in again if a browser still held only that cookie. (#717)
- The `weave_apply_schema` MCP tool takes `partial`, as `PUT /api/schema` and `weave schema apply --partial` already do: a document carrying only a new space adds it and leaves the other spaces in place, so an agent no longer restates the whole workspace to add one (Issue #916).
- The system tables nav test that checks a once-resolved selector against a repaint holds the gap open until the read has run, instead of racing a two-frame refill, so a slow evaluate round trip no longer fails the shard (#837).
- Drag the left nav's right edge to resize it between 200px and 480px; the width is remembered, a double-click restores 264px, and the arrow keys nudge it 16px at a time. The narrow drawer and phone layouts keep their width. (#905)
- On a phone every space header in the left nav is a 44px tap target again; the System head and the space links share one phone rule that outranks the 900px one (Issue #914).
- The foot of the left nav is redesigned. Workflows, Users, Activity and Trash live in a built-in System space pinned under your spaces, with no counts; it folds like any space and remembers the fold. Under it, an identity panel shows your avatar, name and your role in this workspace as a coloured chip, and its menu holds Theme, Keyboard shortcuts and Sign out with the version beside it. The rail loses its ? and theme buttons; the ? key still opens the shortcuts. The rows and size line, the uptime and the version chip are gone; a stale or behind server still warns. (Feature #333)
- The stored system space is named System (its key stays `workspace`), so qualified names read `System/Users`, `System/Spaces` and so on; every workspace is renamed on open, and `Workspace/...` names keep resolving. (Feature #333)
- The nav show and hide buttons draw one panel-left icon on a soft borderless 28px square and sit in the same top-left spot whether the nav is open or collapsed, so the control never jumps when clicked. The titles read Hide sidebar and Show sidebar. (#906)
- The table and the record open the same Fields menu. The eye button on a record used to open a second, smaller menu of switches with no reorder, while the table's opened a checkbox list with grips; one builder now draws both. Every row is a switch with a grip on its left, Show all, Hide all and New field sit on both, and dragging a row or pressing ↑ / ↓ on its grip reorders the view's columns on a table and the table's field order on a record. The Deleted and Σ rollup row switches stay on the table menu only. The record menu opens under its own button, in the dock as on the full page (Issue #910).
- The Fields list keeps its switches clear of the scrollbar. The list reserves its scrollbar gutter whether or not it scrolls and pads its right edge to match the grip's inset on the left, so a switch sits 8 to 12px from the scroll thumb and no row shifts when the list starts scrolling (Issue #909).
- On a phone, a workflow state or select option row in the field tray wraps: grip, icon and a name box at least 140px wide on the first line, and category, colour, preview and a 44px delete button on a second line. (#842)
- A multi-select picker shows a footer with "N selected" on the left and "Done ↵" on the right, so an empty multi picker no longer looks like a single select and its save on close is visible. Done commits what is staged; picked options still leave the list as chips, with no checkboxes. (#798)
- `atRest` joins `styleOf` and `settled` in `test/lib/browser.mjs`: it waits for an element whose transform is identity, whose box has not moved for a frame, and which has no running animation, so a case cannot read a sheet whose spring-back transition has not started yet. The phone picker sheet case waits on it instead of `settled` alone (Issue #901).
- Every field picker opens titled with its field name, and its search hint reads from that title ("Search priority…") instead of one of four hand-typed strings. A set value keeps the hint in place of an empty box. The icon, table, space and term pickers title themselves "Icon", "Table", "Space" and "Term". (#799)
- Show/hide lists in popovers use the checkbox. Rows in the Fields menu, on the table and the record, and in the Σ footer picker were switches; they now hold the same checkbox as the grid's row selector, the Filters popover and the field dialog, on the left of the label. Grips, drag and ↑ / ↓ reorder, the scrollbar gutter, and the Deleted and Σ rollup row entries stay as they were. Settings such as Link preview before sign-in and boolean field values keep their switches (Issue #915).
- On a member-workspace host such as weave.weave.grunion.ai, the default workspace's rail tile now loads its own logo, links to its own home and opens its own menu record, instead of reaching the host workspace through the bare root path. The wordmark and active tile name the host workspace. (#907)
- `PUT /api/schema` and `weave schema apply` take `partial`/`--partial`: a document that adds spaces no longer has to repeat every existing space, and nothing it leaves out is deleted. (#897)
- The security scan's gitleaks step reads only the checked-out history, so a finding on another open branch no longer fails every pull request's required check. (#913)
- The PR gate accepts the security scan test's `gitleaks not installed` skip in lanes without the gitleaks binary, as it already accepts `semgrep not installed`. (#913)
- With billing on, a person with no account can create a first workspace: signing in at `/start`, or at a workspace that answers **No access** (which now offers **Start your own workspace**), lets them create one and become its Architect, working as an Observer until they hold the solo plan. Each login gets one unpaid workspace; another create answers `402` `plan-required` naming the one waiting on a plan. With billing off nothing changes (Feature #331).
- `/start` is a setup page: a left pane walks a new person through Account, Workspace, Template and Agent, and the right pane shows the weave brand and weaves each table in while the workspace builds. Skip setup on any step makes an empty workspace; Set it up myself skips the guided setup straight after sign-in. Sign-in failures come back to the pane with the reason instead of a refusal page. A workspace made here counts as onboarded, so the in-app welcome no longer asks for its name again (Feature #324, Issues #569, #911).
- `WEAVE_OPEN_SIGNUP=1` lets anyone who signs in create workspaces from `/start`, up to `WEAVE_SIGNUP_LIMIT` each (default 1). Without it the page tells a stranger the server is invite only, where it used to end in a 403.
- The Agent step lists chat apps first (Claude, ChatGPT, Gemini, Perplexity), then Cursor, VS Code, Claude Code and any remote MCP client, each with its own steps, the workspace MCP address and a first message to paste, both with copy buttons. It turns green when that app first reaches the workspace over OAuth MCP and passes only connected or skipped (Feature #326).
- `test/setup-flows-browser.test.mjs` walks every setup path in the CI gate that holds production releases, and connects a simulated chat app through protected-resource discovery, dynamic client registration, PKCE and the token exchange, the way claude.ai and ChatGPT do.
- Created At and Modified At read like date fields ("Aug 28, 2026 5:14 PM" in your own zone), with seconds and the zone on hover. Created By and Modified By chips come from the Workspace/Users table: a person shows initials, an agent a glyph, the chip opens the Users row and its hover gives Name, Role and Email. An actor with no Users row, such as `web`, is a quiet chip with no link (Feature #326).
- The starter catalog grows from three templates to fifteen kinds of work, each tagged with the work it tracks: hiring, team projects, product roadmap, content calendar, customer support, freelance clients, research, fundraising, events, inventory, nonprofit donors and home join personal money, personal tasks and sales. The in-app setup cards and the empty-workspace card keep the three featured templates. (#328)
## v0.4.99 (2026-10-15)
- Change an account's role with `account set --role`, `weave_accounts` action `set`, or `PATCH /api/accounts/:ref`. Existing tokens and sessions keep their identity and immediately use the new role; every door, including Users rows, refuses to demote the last architect. (#896)
- The last three create surfaces open in the right-hand tray: the workspace rail's **New workspace**, a table menu's **New share page…** and a space's **Use template**. The first-run setup wizard stays a centered modal (Issue #780).
- The flicker gate ignores editor teardown inside a panel removed in the same update. Connected editor regressions and visible transient subtrees remain checked (Issue #899).
- **The phone picker gesture test waits for the sheet to rest** (Issue #901): wait for the opening animation and short-drag spring to finish before measuring or dragging the grabber again. Check the rendered transform as well as the inline transform so an unfinished spring cannot pass as a settled sheet.
- The sidebar holds its edges: the workspace name and the search button stay at the top however far the space list scrolls, and **+ New space** now sits above the system tables (Workflows, Users, Activity, Trash) in one pinned strip at the bottom (Issue #854).
- The supervisor swap test reads routing order per request loop instead of pooling every loop's completions into one list, so a request that finished out of order on the draining old worker no longer reads as the proxy routing backward (Issue #821).
## v0.4.98 (2026-10-15)
- **The report button sits in the corner** (Issue #846): on a desktop viewport the bug glyph was pinned 40px above the bottom edge while holding 12px from the right, so it read as floating above the corner it belongs in. It now takes the same 12px inset from both edges, the value its phone rule already used. The raised offsets that clear a real neighbour are unchanged: the open tray still pushes it to 72px, and the phone bar still pushes it to 86px.
- On a phone the fields block on a row page is an inset group of 50px rows, label left and value chip right, with a chevron that opens the field's picker sheet and an empty value as a single + chip. The drag grips are gone at that width and a relation that carries chips takes its own full-width row. (Issue #741)
## v0.4.97 (2026-10-15)
- Restored automatic release publication after successful same-repository main CI; existing published versions skip duplicate image and registry publication (Issue #887).
- Table and field descriptions open from a raised info icon, fold after three lines, and remember each viewer's open notes. Empty table descriptions start from Add description in the table menu (Feature #302).
- Temporarily restore inline descriptions while the info-icon feature receives additional phone-menu verification (Feature #302).
- Preserve horizontal access to long grid URLs and date ranges without changing regular date sizing (Issue #897).
- Select and state pickers list real values only. The empty dash row is gone, so Option+1 is the first option anyone wants rather than a blank one, and clearing moved to where it already worked: Backspace on an empty search, or the ✕ on the chip in the search box. A single picker holding a value now names that key in its footer, and every row's quick-pick hint is spelled "Option+1" instead of a bare glyph. (Issue #797)
- Creating a field from the Fields system table opens the field tray directly, with a Table row at the top instead of a centered modal that collected one value and handed off. A column header still opens the tray with no Table row, since the table is known (Issue #779).
- Creating a table happens in one right-hand tray with Name, Space and icon, opened by the Tables grid +, the empty-workspace + New table, the sidebar space + and the space page add. The two centered New table modals are gone, and a table is named before it is created rather than landing as a row called "New table" (Issue #778).
- The grabber on the phone picker sheet and the new-row sheet drags: it follows the finger, dismisses past a third of the sheet or on a flick, and springs back otherwise. One shared helper draws it as a 44px target on both sheets, where before the pill was decoration that promised a swipe and did nothing (Issue #808).
- The reorder grip on a view in the Views menu is a 44x44 target on a touch screen, pulled back with negative margins so the view name and the popover keep their width (Issue #804).
## v0.4.96 (2026-10-14)
- Added tested historical release recovery without moving immutable tags or latest (Issue #887).
- A select or multi-select cell creates a missing option in place: type a value no option matches and the picker offers "+ Create “X”" as its first row, writes the option to the field, then picks it on a select or stages it on a multi-select. An empty one now reads "No options yet. Type to add one." instead of opening to nothing. (Issue #800)
- Sidebar space icons keep a consistent gap before their labels on desktop and phone (Issue #888).
- One state picker everywhere: a workflow field opens the same numbered list from a table cell, a record page field row and a chip's state mark, with thin category dividers (Not started, In progress, Done, Canceled) in place of the grouped grid the chip used to draw. (Issue #796)
- The root workspace's Workspace registry space (Spaces, Tables, Fields, Views, Workflows, Workspaces, Forms, Users) no longer sits in the sidebar beside user spaces. Its tables are listed on the workspace page, under Activity, and open from there; Workflows and Users keep their pinned sidebar rows. The space itself is unchanged and still cannot be deleted. (Issue #874)
- Sheriff now fails its workflow when recovery needs intervention, its revert is unarmed, or tracker credentials are missing (Issue #894).
- A workspace created on an instance with `WEAVE_ORIGIN` set, or whose default workspace requires sign-in, starts with `requireAuth` on, so a guessable `<slug>.<domain>` host never answers an anonymous caller. After every production promotion, `scripts/exposure.mjs` asks each workspace for its tables with no credentials and fails the run on any 200 outside `PUBLIC_WORKSPACES`. (Issue #879)
- Workspace logos that cannot load show the workspace initial in the rail and page header, while keeping the logo replacement and removal controls (Issue #889).
## v0.4.95 (2026-10-14)
- Buttons hover three ways, one per weight, instead of six. A quiet or icon button takes a faint neutral wash and moves its glyph to body colour, so the thirteen `.btn-ghost-secondary` buttons stop flipping to a solid slate block with white glyphs. A bordered button darkens its border to one grey, `--wv-hover-border`, in place of the two greys and the primary blue that different authors reached for. A filled primary darkens one step rather than fading to 80% alpha, so Invite, Done and Create workspace no longer wash out under the pointer. Eight hand-rolled quiet hovers collapse into one rule and the palette is two tokens, `--wv-hover-wash` and `--wv-hover-border`. Gates: `test/button-hover.test.mjs`, `test/button-hover-browser.test.mjs`. (Issue #760)
- A filter value in the table filter popover is the same chip the grid cell draws: the baseline `.k` chip, borderless, 4px radius, 13px at weight 400 and 1px 8px of padding, tinted by its own hue or workflow category. The popover's second chip build (1px border, 5px radius, `6px 10px 6px 8px` padding, secondary font size) is gone, and a picked value now wears `--wv-chip-selected-ring`, one shared 1.5px primary ring, instead of swapping its border and jumping to weight 600. Gate: `test/filter-chip-baseline-browser.test.mjs`. (Issue #769)
- A long filter value in the table filter popover wraps again. The baseline chip holds its label on one line, which a grid cell wants and a filter list does not: the widest option pushed the popover's own content wide enough to cut "Clear all" by 23px. Gate: `test/cutoff-gate-browser.test.mjs`. (Issue #769)
- A picked option looks the same whichever popover is open. The picker drew its cursor chip with a 2px primary outline at 1px offset while a picked filter value tinted its fill, so the same option in the same table had two selected looks. Both now read `--wv-chip-selected-ring`, the 1.5px primary ring Issue #769 introduced. Gate: `test/selected-ring-browser.test.mjs`. (Issue #772)
## v0.4.94 (2026-10-14)
- A row crumb no longer draws its table's icon a second time: on a full entity page the row wears its own record mark, and a hop into another table still shows that table's icon (Issue #746).
- The workspace mark in the breadcrumb bar is drawn at the size of the glyphs beside it: the weave mark paints half the width of its own box, so it read smaller than every lucide crumb icon next to it (Issue #746).
## v0.4.93 (2026-10-14)
- **Comments off in the UI** (Feature #323): a record, on the full page and in the peek, no longer draws the Comments card or its comment box, comments alone no longer open the side column, and the Activity panel drops its Comments chip, its comment rows and their Delete action. Stored comments, `POST`/`DELETE /api/entities/:id/comments`, the MCP comment tools, the `add-comment` automation action and `comment-added` entries in the Activity table are unchanged, so turning comments back on needs no migration. Gate: `test/comments-off-browser.test.mjs`.
- **Dev smoke and soak** (Feature #323): after `Deploy dev` confirms a main commit is serving, the new `dev-soak` workflow runs a Playwright smoke against dev.weave.grunion.ai every three minutes for fifteen minutes while it reads the supervisor's watch block, and posts the commit status `dev-soak` on that SHA: success when the watch passed with no rollback, no worker restart and no 5xx, failure with a link to the run and one alert line otherwise. A failed smoke runs again at once, and only a second failure in a row fails the soak. A newer main commit cancels the older soak. Gate: `test/dev-soak.test.mjs`.
- A List view row's drag grip is a 44 by 44 target on a touch screen, up from about 20 by 20, so a finger can take hold of a row to reorder it. The glyph keeps its size and sits centred in the target, the row height does not change, and a mouse pointer still gets the small grip on hover. Gate: `test/phone-list-grip-browser.test.mjs`. (Issue #805)
- A wait past four seconds captions the page loader with "Still loading this page", and past ten seconds offers a "Try again" control that re-runs the route, the same hand-off the route error card makes (Issue #393).
- The page loader keeps a still woven mark under `prefers-reduced-motion: reduce` instead of looping the rope, announces the wait once as a `role="status"` region reading "Loading the page", and marks `#main` busy while it covers the page (Issue #394).
- Keyboard navigation inside a dialog or tray now reaches a disclosure's summary, and no longer dead-ends on the controls hidden inside a closed one. Tab through the field tray of a formula field reaches "Fields and functions", "As an agent would do it" and the tray's own Save button; Enter on a summary opens the section and the next Tab lands inside it. (Issue #816)
- A grid column's resize edge is a 24px target on a touch screen, up from 6px, so a finger can take hold of it instead of opening the field menu underneath. The painted line stays a 6px hairline, drawn now by the strip's own `::after`, and a mouse pointer keeps the 6px strip it had. On a column narrower than 60px the target stops at two fifths of the header, which leaves the header the room it needs for a long press and a tap. Gate: `test/phone-col-resize-browser.test.mjs`. (Issue #807)
- A row page on a touch screen draws no grip glyph on its field rows, its FIELDS block head or a document section head, and the 16px column that held the glyph is gone, so a label starts at the row edge and a value gets 26px more width. Press and hold the row itself to pick it up, as the Handbook already describes. The grips that are a block's only drag handle, on an attachments block and a related table, keep their place, and a mouse pointer still gets every grip on hover. Gate: `test/phone-row-grip-browser.test.mjs`. (Issue #806)
- **Pull requests run their tests on GitHub-hosted runners before auto-merge** (Feature #319): the new `pr` workflow runs every unit file in four shards and the browser tests the diff maps to (a changed browser test, a test named for the changed module, a test whose source cites the changed file), plus a fixed smoke set, in about twenty files a shard and four shards at most. `PR gate` succeeds only when every shard ran and passed at the exact commit, and it is now a required check on main beside `Dev gate`. A newer push cancels the superseded run. Agents no longer wait on the shared local test manager to land; the full suite still runs on every main push. Gate: `test/ci-test.test.mjs`.
- **Re-running a failed PR shard can turn `PR gate` green** (Issue #883): each shard's evidence artifact now carries its run attempt, and `PR gate` judges every shard by its newest attempt. Before, a re-run that passed still failed the gate, because the failed attempt's artifact overwrote the new one on download. Gate: `test/ci-test.test.mjs`.
- **Production promotes itself from green main** (Feature #322): the new `promote` workflow runs hourly and acts every `PROMOTION_EVERY_HOURS` (default 2) or on dispatch. It picks the newest main commit with a `CI gate` success and a `dev-soak` success that descends from the `production` branch, fast-forwards `production` to it through the promotion deploy key, calls prod's supervisor deploy hook and waits for `/api/health` to show that commit with its ten-minute watch passed. A failed watch or a rollback fails the run, files an Issue row, alerts and opens a revert PR on main for the promoted range; `production` is never rewound. `PROMOTION_PAUSED=true` stops it. Gate: `test/promote.test.mjs`.
- **Releases no longer gate production** (Feature #322): the `release` workflow's publish job leaves the `production` environment and stops waiting on prod health, since prod now follows the `production` branch that `promote` moves. Tags, the GitHub Release, the GHCR image and the MCP registry entry stay available on demand.
- **A red main gets a sheriff** (Feature #320): when a main push `tests` run fails, the new `sheriff` workflow reruns its failed jobs once and logs a green rerun as a flake. A second red bisects the commits since the last green `CI gate` by dispatching `tests` on midpoint commits (the `tests` workflow takes a `ref` input for this), at most four runs, then opens a squash auto-merge revert PR naming the failing tests, the run links and the original PR, comments on that PR, files an Issue row and writes one alert line. It never reverts a revert, never acts on the same culprit twice and never pushes to main. Gate: `test/sheriff.test.mjs`.
- **A refused revert PR still reaches the original PR** (Issue #884): when GitHub refuses the sheriff's revert PR, as it does while `SHERIFF_TOKEN` is unset because Actions may not open pull requests here, the sheriff comments on the original PR, files the Issue row and alerts with the pushed branch and its compare link, where it used to fail with only its failure alert. Gate: `test/sheriff.test.mjs`.
- **Re-run failed jobs can turn the main CI gate green** (Issue #883): the main `tests` shards upload `ci-<shard>-<attempt>` and `aggregate` judges each shard by its newest attempt, as the PR gate does since #122. Before, the failed attempt's result overwrote the rerun's, so a flake that passed on rerun still read red, and the sheriff would have treated it as a real failure. Gate: `test/ci-test.test.mjs`.
## v0.4.92 (2026-10-12)
- **A reference chip in the editor sits inline where its markup was** (Issue #878): an aliased `[[Issue#229|Long label]]` drew its chip over the raw markup, which kept rendering underneath, so its tail (`pped]]`) leaked past the chip into the next words, the `[[` peeked out below it, and a reference that wrapped to a second line stayed raw. While the caret is outside a reference its markup now takes no visible space and keeps exactly the chip's width, so the line reflows around the chip; a reference that wraps gets its chip too, and a chip wider than its line ends in an ellipsis. With the caret inside, the markup shows for editing. The stored markdown is unchanged. Gate: `test/doc-ref-chips-inline-browser.test.mjs`.
- **A request entity id never reaches Object.prototype in the hub route switch** (Issue #881): a member route that falls back to the root for an entity it does not hold now checks both workspaces with `Object.hasOwn`, so an id of `__proto__`, `constructor` or `toString` answers the member's own 404. The semgrep result that kept the security workflow red since 25be20b is fixed in code, not baselined.
- An anonymous form submission from a browser that had no client cookie yet now records its receipt under the cookie the answer mints, so a retry with a fresh idempotency key replays that receipt instead of filing a second row (Issue #811).
- A write that lands while another signed-in request is still arriving now records the account that sent it, instead of whichever request reached the server last. Row authors, comment authors, bug-report reporters, template builds and credential ownership checks all read the actor captured when the request authenticated (Issue #810).
- A shared table page now shows the rows the table's default view shows: its filters, its saved search and its sort order, the same configuration the table page opens with. It used to list every row in the table, so a viewer saw rows the owner had filtered out (Issue #838).
- The self-updating supervisor now watches each new release for ten minutes (`WEAVE_WATCH_MS`). A serving worker that exits, three failed health checks in a row or a 5xx rate past `WEAVE_WATCH_5XX_RATE` swaps back to the kept good release, unless that release's deferred migration already wrote to the workspace, which raises an alert instead. A boot that fails falls back to the newest good release before the image, and `WEAVE_TRACK=branch:<name>` follows a promotion branch (Feature #321).
## v0.4.91 (2026-10-12)
- The record page and the peek fetch a record's references beside the record itself, so a redraw of the same record keeps its scroll and the side column never jumps. The table badge on a relation chip reads at 4.5:1 contrast in the References card (Feature #315).
- The Activity panel opens inside the record as a column right of its body, on the full record page and in the peek beside a table. The record body narrows to make room; the page, the crumb row and the table no longer move or sit under the panel. On a phone it is still a full-screen sheet (Issue #871).
- Activity shows in two places: the Activity button in the record's toolbar, which opens the panel inside the record, and the Activity system table at #/activity. The Activity card in the record's side column, the Activity system field (its table column and its show/hide row) and each document's History button are gone. Tables that stored the Activity switch lose it on load. The References and Referenced by cards load with the record and open the side column on their own, as do comments. `weave_query` no longer takes `Activity` in `fields`; `weave_get_entity` and `weave_activity` still carry the history (Feature #315).
- **Backup to weave cloud** (Feature #316): `weave backup --dest weave://<host>/<account>` and `WEAVE_BACKUP_DEST=weave://…` send the sealed nightly archive to a weave cloud account, authenticated with `WEAVE_BACKUP_TOKEN`, a `wv_` token from that account. The cloud stores ciphertext only. A cloud deployment with `WEAVE_BACKUP_BUCKET` set answers `PUT /api/backups/<archive>`, `GET /api/backups` and `GET /api/backups/<archive>`, keeps each account to thirty archives and `WEAVE_BACKUP_QUOTA_BYTES` (5 GiB by default), and refuses anonymous and cross-account callers. `weave restore --from weave://<host>/<account>[/<archive>]` pulls the newest or the named archive. `/api/health` names the destination kind. The new Handbook page **Backup to weave cloud** covers the destination, token, quota, restore and doors.
- **Migrate to weave cloud** (Feature #316): `POST /api/workspaces` with `{ name, from: "backup:<archive>", key }` creates a cloud workspace from an archive the caller's account stored, decrypting it in a scratch directory with the supplied passphrase and running the existing restore. The key is never stored or logged. A wrong key answers 400, another account's archive 404, and a second migration of the same archive 409. `weave workspace migrate --from weave://<host>/<account>[/<archive>]` is the CLI form.
- **A GHCR image and an Official MCP Registry entry on every release** (Feature #292): the approved release pushes `ghcr.io/grunion-ai/weave:<version>` and `:latest`, labelled `io.modelcontextprotocol.server.name=io.github.grunion-ai/weave`, then publishes `server.json` (the image over stdio, running `node bin/weave.js mcp`) with a pinned `mcp-publisher` signed in through GitHub OIDC. The release fold stamps the version into `server.json`. The README Quickstart shows `docker compose up -d` beside the node one-liner.
- `GET /api/health` again answers `{ok, name}` to an anonymous caller, as Issue #520 set out: the release supervisor confirms a new worker by the version that worker reports over its private IPC channel, so a walled instance takes releases without showing its version to the public internet (Issues #520, #843 and #844).
- A supervisor serving the image it shipped in reports the commit Railway built it from in `/api/health` (`supervisor.commit`), so production names its build the way dev already does (Feature #305).
- **KaTeX 0.18.11** (Issue #758): the vendored math engine moves from 0.16.47, which carries advisory GHSA-238p-pmpm-9mq7 (fixed in 0.18.2), to 0.18.11. Only `katex.min.js` and `katex.min.css` change; the mhchem extension and the twenty woff2 fonts are byte-identical in the new release. `security/vendor.lock.json` is rehashed and `scripts/vendor-advisories.mjs --require-all` reports no advisory.
- **The phone nav drawer holds the page** (Issue #262): below 900px, while the menu is open, everything outside it is inert, Tab and Shift+Tab stay inside it, focus goes in on open and back to the menu button on close, and the menu button carries `aria-expanded`. Every way the menu closes (Escape, its close button, the scrim, a nav link, a hash change, the window growing past 900px) frees the page. Shift+Enter, ⌘Z and `?` stand down while it is open.
- **Workflows stays in every sidebar** (Issue #875): a member workspace served by the hub keeps its Workflows row beside Activity and Trash. The row finds the Workflows table through the root registry and opens it on the root, and it still shows, linking the root registry, when no Workflows table resolves.
- **The tray, the full-screen viewer and the ⌘K palette hold the page** (Issue #263): each is a labelled modal dialog, Tab and Shift+Tab stay inside it, the app behind is inert while it is open, and focus goes back to the opener when it closes. Holds stack, so ⌘K over a dialog holds that dialog too and hands it back, and the tray replacing a modal keeps the page held. The palette's Tab still jumps to the next group, and it frees the page before a caller's `onPick`, `onDismiss` or `onCommit` runs. Shift+Enter (quick-create) stands down while the tray or the viewer is open.
- **A phone list row never draws a chip cut off** (Issue #876): a chip whose value is wider than its slot, such as a relation to a long-named row, leaves the line and the row counts it in its +N like any other chip that does not fit. A row whose only chip leaves shows the count alone. An empty date is no chip, like every other empty field, so its placeholder never shows half drawn.
- **Prefill links and the form editor choose through the picker** (Issue #860): the New row form a prefill link opens and the form editor's Audience, choice default and Yes/No default no longer draw native selects. Each opens the search picker every other selector uses, and a multiselect prefill or default picks several values in place through a new multi face on the form picker.
- **Release folder retention** (Issue #873): `weave supervise` with `WEAVE_AUTO_UPDATE=1` now deletes old folders under `releases/` after every swap, every failed health check and every boot. It keeps the folder it serves and the most recently served good one for rollback, or only the newest good folder while the image serves. `releases/state.json` stays, and a folder it cannot remove is logged without blocking the swap. Production's volume had filled with three release folders, one of them a failed v0.4.89.
- **A malformed percent-escape in a path answers 400 everywhere** (Issue #869): `decodePath` now throws an `invalid` WeaveError for a bad escape such as `%E0%A4%A`, so the Cloudflare worker answers 400 where it answered 500. A sparkline style outside `line`, `column` and `winloss` draws a line instead of reaching the SVG markup. The list-marker conversion and the editor's reference chips build DOM nodes instead of parsing HTML strings.
- **Cool slate surfaces** (Feature #317): the canvas, panel, sunk surface and dividers move from Kami paper and navy to a neutral slate in both themes, on the app, the applet and the auth page. Text, links, primary and the rope's strands keep their colours. `brand/README.md` carries the surface table every future design reads, and `test/surface-palette.test.mjs` keeps the app on it.
- **Users system table** (Feature #318, Issue #852): every account and pending invite is now a row of Workspace/Users, pinned in the sidebar under Workflows. A new row with an Email invites that person at its Role and answers with the one-time link; a new row with Kind Agent makes an agent account and answers with its token once. Editing Role or Name changes the account, deleting the row revokes the invite or deletes the account, and any table can relate to Users. Only an architect writes Users rows, and no row, query, export or MCP answer carries a token, session or invite hash.
- Each workspace's Workflows row opens that workspace's own workflows at its own address, and the root lists only its own. The server scopes the Workflows query, row list and trash to the asking workspace, a member cannot read another workspace's workflow by id, and existing rows take their workspace from the table their rule runs on (Issue #877).
## v0.4.90 (2026-10-12)
- Once a workspace, or the hub root it belongs to, holds an account, a caller with no token and no session is capped at observer even while the wall is off: reads, comments, form submits and bug reports still answer, and every other write answers 401. A hub with no accounts anywhere keeps today's open local use (Issue #534).
- dev.weave.grunion.ai follows main: ready pull requests land after a sub-minute Dev gate, the supervisor swaps dev to each main commit with health-checked rollback, and production releases wait for an approved run (Feature #305)
- **The container image is pinned by digest and runs on code it cannot change** (Issue #524): `FROM` names `node:22.23.2-slim` by its `sha256` digest, so a build gets the bytes that were reviewed rather than whatever the tag points at that day; the application tree is copied as root and the `node` user that runs it can only read it; `.dockerignore` matches `*.db`, `files/`, `node_modules` and the other state patterns at every depth instead of the repository root alone. `test/security/dockerfile-hygiene.test.mjs` guards all three.
- One rail across several logins. The start page's cookie now holds every login this browser has signed in with, and `/start` lists the union of the workspaces those logins open, each row naming the login that opens it once there is more than one. "Add login" sends the person back to the provider with its account picker and adds the result; "Sign out" forgets them all. Every sign-in, at the start page or at a workspace, runs find my workspaces and sets a `wv_rail` cookie for the whole base domain naming the workspaces found, so a workspace host draws chips for workspaces it holds no session for; such a chip opens through `/api/start/open/<slug>`, which picks whichever held login has an account there. The rail's + button is now a menu: New workspace, Add login, Find my workspaces. An invite link opened in a browser that already holds a login offers "Accept as <login>" for each one, which redeems the invite with no provider trip, or "Use another login". A new device starts with the docs workspace only and rebuilds the rail one sign-in at a time; no server-side record links logins (Feature #279).
- `test/find-workspaces.test.mjs` covers the login union and labels, the rail cookie, opening with the right login, held chips on a workspace host, the auto-run on a plain sign-in, the invite chooser and sign-out; `test/find-workspaces-browser.test.mjs` drives the start page across two logins and the rail menu in both themes (Feature #279).
- **AES-GCM decryption demands the full 16-byte tag** (Issue #533): the keystore and the backup archive both passed `setAuthTag` without telling `createDecipheriv` how long the tag must be, and Node then accepts a tag cut to 12, 8 or 4 bytes, each cut lowering the forgery cost. Both sites now pass `authTagLength: 16`; `test/security/gcm-tag-length.test.mjs` proves a keystore entry with a 12-byte tag is refused.
- **Header crumbs stay whole beside the new toolbar buttons** (Issue #863): on a desktop the Sort button that #69 added to the table toolbar draws as an icon button (its name stays as a tooltip and in the phone tools sheet), and the dock header packs its icon buttons tighter, so the table crumbs at 1440px and the current crumb in a 550px dock are no longer cut off.
- `GET /api/health` names the running version to an anonymous caller again, so the production supervisor, which waits for a candidate's health to report its own version, can swap in a new release on a walled instance. The build, sizes, workspace and backup state still need an architect (Issues #843 and #844).
- Typing "- ", "* ", "+ ", "1. ", "1) ", "- [ ] " or "- [x] " at the start of an empty line in a document turns it into a list or task item on the space, not on Return. Backspace right after gives the typed marker back, and nothing converts inside code blocks or table cells (Issue #750).
- The loader now leaves on the woven mark instead of waiting out its 2 s cycle: on finish it fast-forwards the rope to the next woven pose at 3x, holds 120 ms and fades over 160 ms, so a page that finished mid-cycle is visible within about half a second of its last response instead of up to 1.9 s later. The grid loader exits the same way. (Issue #392)
- **The core MCP tool list fits its budget again** (Issue #857): stored positions (#36) and the forms tool pushed the default `tools/list` to 15,478 bytes, past its 15,400-byte cap. The `weave_call` one-line summaries and the `weave_workspace` logo clause are shorter; no tool loses an argument or a capability.
- Every action menu now opens through one builder and wears the column header look: the ⋮ menus on tables, records, spaces, share pages, documents and decks, the nav table ⋮, the column header menu, the workspace tray menu, the account menu and the folded breadcrumb menu. Focus lands on the first row; the arrow keys, Home, End and typeahead move it; Escape and Tab close the menu and return focus to its button. Rows carry `menuitem` roles. On a phone the same menu opens as a bottom sheet with 44px rows, and the table sheet's actions open from a Table actions row. Items, labels and order are unchanged (Feature #296, Issues #762, #763).
- Every object kind has one menu, and every way of opening it shows the same rows in the same order: the ⋮ button, a right-click on the page header or the sidebar row, and the workspace chip on the rail (Feature #296, Issues #761, #764).
- Menus run in four sections, Settings, Organize, Share and Export, with Delete last. Export is the one labelled section: each row names a format (Markdown, Mermaid, HTML, PDF, CSV, JSON) with its extension beside it.
- Rows that belong elsewhere left the menus. **Sort** shows on the desktop toolbar too, so a column's ⋮ no longer sorts. A record's Activity and Show or hide fields buttons, and its copy link, show in the dock and on a phone. Rename a table by editing its title or double-clicking its sidebar row; change a workspace logo by clicking it on the workspace page. Link preview before sign-in is a switch in the workspace menu. A deck's download, model, and new version controls are buttons on its panel. Column settings read **Field settings…**.
- Every menu deletes in one click: tables, spaces, records, share pages and fields show **Move to trash**, and the **Moved to trash** toast's **Undo** puts the object back where it was. Tables and spaces no longer ask for a hold, and fields and share pages no longer delete for good. The selection bar's trash uses the same words and Undo. A space's sidebar row gains the ⋮ every other row has (Feature #296).
- **Delete workspace…** has left the workspace menu and its confirm dialog is gone until Settings takes it; `DELETE /api/workspaces/:id` and the Trash page's Restore stay (Feature #297).
- A share page restored from the trash takes back its old place on the workspace page instead of landing last (Issue #855).
- **The vendored mermaid bundles DOMPurify 3.4.16** (Issue #559): `public/vendor/mermaid.min.js` is a fresh build of mermaid 11.17.2 whose DOMPurify is 3.4.16, past GHSA-55q2-fjhq-7xh7 (fixed in 3.4.13); every published mermaid bundle through 12.1.0 still carries 3.4.12. `security/vendor.lock.json` records the build recipe, and `test/security/mermaid-dompurify.test.mjs` fails on a bundled DOMPurify inside the advisory range.
- Forms are managed inside a table through its Forms action: choose fields, labels, required values and defaults, then open, copy, edit or pause the same permalink. The editor preserves server-filled values and follows the existing Workspace/Forms registry (Feature #303).
- Native form management has HTTP, CLI and MCP equivalents, with architect checks on the workspace and its registry and privileged access to hidden configuration, including accountless workspaces with sign-in required (Feature #303, Issue #815).
- Form field sections are keyboard-accessible, and unrelated edits retain multiline and partial-date defaults (Issues #816, #817).
- Spaces, tables and the workspaces in the rail keep a stored order. A `position` (0 is the first place) moves a space or a table on every door: `weave_update_space`, `weave_update_table` and `weave_move_table`, `PATCH /api/spaces/:space`, `PATCH /api/tables/:table` and `POST /api/tables/:table/move`, and `weave space update`, `weave table update` and `weave table move` with `--position N`. The workspaces' order lives on the hub root: `weave_workspace` order and move, `PATCH /api/workspaces/:name`, and `weave workspace order` and `weave workspace move`. A workspace saved before this keeps the order it showed, and a moved table lands last in its new space (Feature #284).
- The phone new-row sheet names the row after its table when the table has no row term ("New task", "Name this task" on a Tasks table) instead of "New row"; the phone New button reads the same (Issue #792)
- The phone new-row sheet opened from a table or view has no table chip, so the row always lands in that table; the chip stays only for a create with no table in context (Issue #793)
- On a phone the new-row Name is a single line whose Return key reads Done and creates the row through the Create button, never while the name is empty or an IME composition is open; the desktop sheet keeps its two-line Name (Issue #794)
- **The workspace logo and the row permalink button keep a 44px tap target on a phone** (Issue #864): the click-to-edit workspace logo (#69) is a 44px box on a phone or coarse pointer, and the permalink button that #69 brought into the p