Pad v0.16.0
Highlights
Relation fields
A Car has a Color, and Pad knows it. A relation field points at one item in another collection; multi_relation holds an ordered list. v0.16.0 makes that a first-class edge everywhere the value can be touched.
- Pick, don't paste. The properties panel shows a linked chip (
COLO-3 · Purple) and a collection-scoped, keyboard-navigable picker, recent first. A reference to a deleted item renders as "(deleted)", never as a bare id. - Create from the picker. Type a colour that doesn't exist and choose "Create 'Purple' in Colors".
- Referenced by. The target item shows who points at it, backed by a materialised index that every write maintains, so the count survives edits, moves, deletes and restores. Upgrading backfills it once, guarded by a completion marker; a normal restart does not re-derive it.
- Filter and group by a relation. Any collection view; the hard-coded tasks→plans parent filter is now the general case. Grouping by a multi-valued relation shows one lane and says so.
- Agents write refs, not ids. CLI and both MCP transports accept
COLO-3or an exact title in the target collection; reads hydrate{id, ref, title}, with per-target visibility redaction. - Bad references are refused at the door. All eight write paths validate that a relation resolves to a live item in the declared collection; stored values render honestly on read.
Writes that cannot lose data
expected_seq, a concurrency token that can tell two same-second writes apart.updated_athas one-second resolution; every item now carries a per-workspaceseqthat bumps on every observable write, and the CLI, web client and MCP send it.expected_updated_atstill works and is documented as the weaker token. The token landed at tool surface v0.34; this release ships v0.35.- Per-key patches at twelve doors. Board moves, URL stamping, bulk operations, GitHub link/unlink, reconcile, and the browser MCP
pad_item update(which used to replace the whole fields blob from the keys you named) now merge per key, so a concurrent edit to another field survives. Two doors in the same class are not covered:pad item noteandpad item decidehave no patch door to move to, and still write the whole blob (BUG-3056). content_statemarks a body the server knows is behind the live document. Every response that carries an item body, including playbook runs, bootstrap bodies, artifact exports, MCP resources, backlink snippets and the CLI's printed bodies, says so when the collaborative op-log is ahead of the row.pad db migrate-to-pgrefuses to abandon unflushed edits, and the abandoned file refuses writes. The migration carries no op-log, so the command lists the affected items, names the remedy (open them in the web UI), and stops with nothing migrated;--discard-unflushed-editsoverrides. A completed migration then marks the source database so it refuses to open, and installs triggers so a writer that was already blocked on the migration's lock is refused and logged when it resumes. A failed run leaves the source unmarked and re-runnable.- Workspace export marks stale bodies and the cross-workspace copy warns when its source was stale.
Also in this release
- Item reminders. Fire-at-an-instant on any item, with one overdue rule across the four surfaces.
- Comments under the item content, with a Comment action on the selection toolbar that quotes into the composer; Activity keeps the change history.
- API tokens from the CLI.
pad token create / list / revoke / rotate. - Attachments. Markdown and plain-text attachments render in the viewer; extension trust covers the audio/video split, the text family, the CFB office trio and RTF.
- Offline. A cold load renders the board from cache, fenced by scope.
- Access changes evict caches. A revoked or vanished workspace is asked about once, not once per tick, and never purges a workspace you can still read.
- NUL bytes cannot enter the database. Enforced at the driver and as a database invariant, with a repair path for legacy rows.
- Collection trait uniqueness is a database invariant. De-dup pass plus partial unique indexes.
- Board and list views survive odd values. A field retyped under existing data no longer takes the view down; grouping by 0 or false keeps the item; lane keys write the field's real type.
- Sign out is reachable from the user menu on every screen height.
Upgrading
- Schema migrations run on first start: five on SQLite, four on Postgres. Back up first:
pad db backup. - Agents on the MCP tool surface: v0.35 adds
expected_seqandcontent_state, and nothing is removed or retyped. One behaviour change: a schema DEFAULT that fails its own type check is now discarded and named inwarnings.dropped_fieldsrather than stored, so a field whose declareddefaultandtypedisagree stops appearing on newly created items. That key used to name only a relation case; it can now name a field of any type. - If you migrate SQLite to Postgres, flush pending edits first or the command will tell you which items need it.
Install
brew install --cask perpetualsoftware/tap/pad
Or docker pull ghcr.io/perpetualsoftware/pad:0.16.0, or grab a binary below.
Nix: nix run github:PerpetualSoftware/pad/release.
Verifying
Every archive is covered by a signed checksums.txt and a SLSA build provenance attestation.
cosign verify-blob \
--certificate-identity-regexp "^https://github.com/PerpetualSoftware/pad/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle checksums.txt.sigstore.json checksums.txt
gh attestation verify pad_0.16.0_linux_amd64.tar.gz --repo PerpetualSoftware/pad
The container image is signed too:
cosign verify ghcr.io/perpetualsoftware/pad:0.16.0 \
--certificate-identity-regexp "^https://github.com/PerpetualSoftware/pad/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
Changelog
Features
- 14cb975: WIP feat(server,store): referent validation for relation values — 6 of 8 doors (TASK-2878) (#1246) (@xarmian)
- eca31ef: feat(attachments): add the viewer text loader with two independent size gates (@xarmian)
- cd2bf7f: feat(attachments): claim markdown and plain text for an in-app text renderer (@xarmian)
- d83632e: feat(attachments): extension trust — the audio/video split, the text family, the CFB office trio and RTF (TASK-2976 / BUG-2963 PR B) (#1309) (@xarmian)
- 7f640a9: feat(attachments): render markdown and plain-text attachments in the viewer (@xarmian)
- f262449: feat(cli): pad token create/list/revoke — CLI mint path for API tokens (#1237) (@b4rk13)
- 1a041b3: feat(cli): pad token rotate — new secret in place, old one dies with the write (#1237 follow-up) (#1330) (@b4rk13)
- cece78a: feat(collab): applier-availability pre-check on RoomManager (TASK-2987 / PLAN-2975 unit 1) (#1314) (@xarmian)
- 185194c: feat(items): content_state marks a body the server knows is stale (BUG-3000) (#1343) (@xarmian)
- d2973b2: feat(items): expected_seq — an OCC token that can tell two same-second writes apart (BUG-3037) (#1351) (@xarmian)
- d5085bb: feat(items): multi_relation — an ordered list of references (TASK-2999 / PLAN-2857 U4) (#1342) (@xarmian)
- 4b6e321: feat(mcp): bump ToolSurfaceVersion to 0.27 (BUG-2850) (@xarmian)
- dd919dd: feat(mcp): carry the fields object with its JSON types intact (BUG-2850) (@xarmian)
- 100da86: feat(nav): surface the repo link in the sidebar footer (@xarmian)
- 162eccb: feat(relations): exact-title resolution and hydrated reads for relation fields (PLAN-2857 U6 / TASK-2996) (#1326) (@xarmian)
- 63adbeb: feat(relations): materialised reverse index and Referenced-by (PLAN-2857 U5 / TASK-2997) (#1328) (@xarmian)
- dc3fc2d: feat(server,cli): name undeclared field keys on the write response (BUG-2850) (@xarmian)
- 3baf984: feat(server,store): the differential test's three live legs + the 400 mapping (@xarmian)
- 00d650a: feat(server,web): detect a revocation that writes no item, and evict the cache it left stale (IDEA-2898) (#1261) (@xarmian)
- 7659ad3: feat(server,web,cli): say when a relation's copy target is unusable, instead of offering a picker that cannot answer (IDEA-2899) (#1262) (@xarmian)
- ebe40de: feat(store): add a dialect accessor for a column's scanned byte length (@xarmian)
- a0e1bbf: feat(store): composite FK making a reminder's workspace agree with its item's (IDEA-2883) (#1252) (@xarmian)
- 52c480c: feat(store): enforce the NUL invariant at the driver (DOC-2823 S1 Layer A) (@xarmian)
- 054b192: feat(store): make the NUL invariant a property of the DATABASE (DOC-2823 S2) (@xarmian)
- 57b7ca5: feat(store,cli): ask the destination about suspects instead of dropping them (BUG-2810) (@xarmian)
- 47d2b15: feat(store,server): make collection trait uniqueness a database invariant — de-dup pass + partial unique indexes (TASK-2710) (#1257) (@xarmian)
- 63da2f4: feat(store,server,cli): count and repair the legacy NUL population (BUG-2810) (@xarmian)
- 346a5c9: feat(web): Comment action on the selection toolbar, quoting into the composer (IDEA-2843) (@xarmian)
- cd51d8f: feat(web): Community link (GitHub Discussions) in the user menu and auth footer (TASK-2888) (#1251) (@xarmian)
- 3d78e1d: feat(web): a cold offline load renders the board from cache, fenced by scope (TASK-2946) (#1287) (@xarmian)
- 7aef246: feat(web): comments move under the item content; Activity keeps changes (IDEA-2843) (@xarmian)
- a850169: feat(web): extract the add-relationship search into a shared ItemPicker (TASK-2862) (@xarmian)
- 39cdfdd: feat(web): filter and group collection views by a relation field (TASK-2998 / PLAN-2857 U7) (#1327) (@xarmian)
- cd5c570: feat(web): give ItemPicker a source model; keep the Relationships tab on server FTS (TASK-2862) (@xarmian)
- 9363dbb: feat(web): imperative appendMarkdown handle on CommentEditor (IDEA-2843) (@xarmian)
- e331342: feat(web): relation fields create their target inline, permission-gated (TASK-2877) (@xarmian)
- a04233a: feat(web): relation fields render a linked chip and edit through the picker (TASK-2868) (@xarmian)
- 322b461: feat(web): the scoped picker offers an inline create row (TASK-2877) (@xarmian)
- b437cc5: feat: item reminders — the fire-at-an-instant primitive, and one overdue rule for all four surfaces (IDEA-2641, closes #1010) (#1244) (@xarmian)
Bug fixes
- 6a5eb3d: fix(attachments): HEIC, HEIF and AVIF are recognised from their bytes, not refused (BUG-2961) (#1291) (@xarmian)
- 734b53f: fix(attachments): a HEIC embed is decided by what can be served and painted, not by an image/ prefix (BUG-2964) (@xarmian)
- cbfc073: fix(attachments): make allowlisted formats reachable by recognising their magic (BUG-2963 PR A) (#1304) (@xarmian)
- 5aa4bbe: fix(build): give each worktree its own Postgres test port, and refuse to run when it is unreachable (TASK-2708) (#1253) (@xarmian)
- 1100455: fix(build): make install proves what it installed and what it restarted (BUG-2897, TASK-2787) (#1272) (@xarmian)
- 39a8366: fix(cli): filter suspects by table BEFORE asking the destination (BUG-2810) (@xarmian)
- d9f3fe3: fix(cli): filtering suspects out of the check also filtered them out of the report (BUG-2810) (@xarmian)
- 5d29b77: fix(cli): item show --format markdown emits the body verbatim (IDEA-2937) (#1281) (@xarmian)
- e4415dd: fix(cli): name the skipped-table suspects instead of counting them (BUG-2810) (@xarmian)
- 4618876: fix(cli): pad server stop signals only a process it can prove is ours (BUG-2969) (#1299) (@xarmian)
- 5ec17a7: fix(cli): pad server stop stops the server that is running, or says it is (BUG-2965) (#1298) (@xarmian)
- e712ca1: fix(cli): pad token create --scopes validates and JSON-encodes (#1237 follow-up) (#1329) (@b4rk13)
- 43d94a3: fix(cli): pass PostgreSQL connection strings to backup clients (#1289) (@mattfaltyn)
- a65252d: fix(cli): print the NUL report on stdout so it can be captured (BUG-2810) (@xarmian)
- 42c08b3: fix(cli): refuse SQLite self-restore (#1332) (@mattfaltyn)
- 02846a6: fix(cli): the session's registered agent is the name its writes carry (BUG-2882) (#1248) (@xarmian)
- e89c8c8: fix(cli): two ways the preflight and its remedy disagreed with the migration (BUG-2810) (@xarmian)
- 3a00ea9: fix(cli,mcp): item update --parent "" is refused, not silently ignored (BUG-2941) (#1285) (@xarmian)
- ee0d945: fix(cli,mcp): one --field key=value entry means one thing at every door (BUG-2870) (#1283) (@xarmian)
- fc32147: fix(cloud): reconcile before destroying a workspace whose owner-add lost its ack (BUG-3026) (#1341) (@xarmian)
- 7dc4931: fix(db): a finished SQLite→Postgres migration marks its source so the abandoned file refuses writes (BUG-3072, BUG-3077) (#1361) (@xarmian)
- 5e869b2: fix(export): mark bundle bodies that were stale when written, and refuse the migration that would abandon them (BUG-3032) (#1356) (@xarmian)
- 1e7013a: fix(items): a lost commit ack no longer writes the item twice (BUG-2994) (#1340) (@xarmian)
- a8f6fa7: fix(items): an injected schema default takes the same type check a supplied value takes (BUG-3079) (#1362) (@xarmian)
- 80e32b4: fix(items): eleven hand-built doors carry the stale-body marker (BUG-3033) (#1344) (@xarmian)
- 0d0f1c9: fix(items): require and bound item titles at every write door (BUG-2833, BUG-2831) (@xarmian)
- 70e3659: fix(items): twelve item-field writers send a patch instead of replacing the blob (BUG-3049 / BUG-3055) (#1349) (@xarmian)
- 6114954: fix(links,store): decide bracket qualification by the old title (BUG-2830) (@xarmian)
- 69a6ebf: fix(links,store): make the rename cascade escape-aware in both directions (BUG-2805) (#1225) (@xarmian)
- b6afbb8: fix(links,web): align the JS wiki-link grammar with Go's
.(BUG-2834) (@xarmian) - af686c3: fix(mcp): a blank top-level param does not block the fields answer (BUG-2850) (@xarmian)
- a1b8e63: fix(mcp): a nil top-level value is absence, for every key (BUG-2850) (@xarmian)
- 3696686: fix(mcp): a padded entry colliding with a param is not an equal duplicate (BUG-2850) (@xarmian)
- 0a71ad3: fix(mcp): an empty parent param is not a hierarchy directive (BUG-2850) (@xarmian)
- baaa236: fix(mcp): apply the field-array conflict guard to promoted keys too (BUG-2850) (@xarmian)
- b23c0db: fix(mcp): apply the null and hierarchy guards to promoted keys too (BUG-2850) (@xarmian)
- 052850f: fix(mcp): both gates ask the per-key question; compare like with like (BUG-2850) (@xarmian)
- 130c854: fix(mcp): canonicalization is a per-KEY property, not a per-request one (BUG-2850) (@xarmian)
- 4937fd8: fix(mcp): canonicalize when ANY entry for the key is padded (BUG-2850) (@xarmian)
- dfee138: fix(mcp): close four codex round-6 findings in the fields-object merge (BUG-2850) (@xarmian)
- 13892fe: fix(mcp): close two codex round-7 findings on the same seam (BUG-2850) (@xarmian)
- eb37c0e: fix(mcp): give the canonical pass full reach; equal structures collapse (BUG-2850) (@xarmian)
- f15f608: fix(mcp): guard hierarchy pseudo-keys and correct the fields description (BUG-2850) (@xarmian)
- 21a3057: fix(mcp): keep per-entry multiplicity in the conflict pass (BUG-2850) (@xarmian)
- 56ee3a7: fix(mcp): require strings for fields.assign/role; fix the alias refusal's mechanism (BUG-2850) (@xarmian)
- a357b9f: fix(mcp): restrict the remote transport to the protocol era pad can serve (TASK-2977) (#1310) (@xarmian)
- a9f2405: fix(mcp): the compat exception turns on a top-level value, not on the key (BUG-2850) (@xarmian)
- dae7bbf: fix(mcp): the same-name exemption holds only where the doors agree (BUG-2850) (@xarmian)
- c2bb1ba: fix(mcp,server): close three codex round-11 findings, one of them my own bad refutation (BUG-2850) (@xarmian)
- 2cf9f00: fix(mcp,server,cli): three codex round-2 findings (BUG-2850) (@xarmian)
- 38d8803: fix(nix): gate the vendorHash heal on state, not on the push range (BUG-2974) (#1303) (@xarmian)
- 89a9fb0: fix(server): a refused collection prefix reaches the caller as a 400 naming the rule (BUG-2951) (#1293) (@xarmian)
- 8d3e389: fix(server): a stream ends when the credential that opened it stops being valid (BUG-3007) (#1323) (@xarmian)
- d201ebb: fix(server): a successful applier-path PATCH stops answering with the previous content (BUG-2995) (#1320) (@xarmian)
- cdc5b30: fix(server): minting or rotating an API token requires an interactive session (BUG-2890) (#1267) (@xarmian)
- a2bab75: fix(server): the op-log prune rides inside the write it justifies (BUG-2840 half B) (#1295) (@xarmian)
- dc70ff3: fix(server): write first, apply second on the collab applier path (TASK-2989 / BUG-2840 half A) (#1318) (@xarmian)
- bb8ec04: fix(server,store): both workspace mint doors enforce their preconditions from one place (BUG-2809) (#1268) (@xarmian)
- 178b6b5: fix(server,store): two more from codex rounds 3 and 4 (BUG-2810) (@xarmian)
- ae793e6: fix(server,store,items): coerce field values to their declared types server-side (BUG-2850) (@xarmian)
- e32d4f4: fix(store): a relation value pointing at an orphaned item is carried, not rewritten into a minted id (BUG-2895) (#1265) (@xarmian)
- 6d4c3b4: fix(store): an operational SQLSTATE is not a verdict about the value (BUG-2810) (@xarmian)
- 37f26f5: fix(store): close the outbox bound's remaining unbounded paths (BUG-2827) (@xarmian)
- 5c4fa22: fix(store): collection slug races — allocate under the row lock, serialize CreateCollection with rename (TASK-2885) (#1249) (@xarmian)
- 12ea3f4: fix(store): compare trigger DEFINITIONS, and inspect inside the transaction (@xarmian)
- 0836808: fix(store): drop a past-the-hop-bound event before judging its size, and correct three comments (BUG-2827) (@xarmian)
- 5cb2297: fix(store): export soft-deleted collections so their live items survive a round trip (BUG-2884) (#1255) (@xarmian)
- 6428e7d: fix(store): lock, stamp and preserve on the relation retarget (BUG-2873) (@xarmian)
- 4687c46: fix(store): migrate relation fields when their target collection is renamed (BUG-2873) (@xarmian)
- 8910ad0: fix(store): mint the imported workspace inside the import transaction (BUG-2892) (#1259) (@xarmian)
- 499387e: fix(store): never regress a migrated token; keep large integers intact (BUG-2873) (@xarmian)
- 214b888: fix(store): normalize IF NOT EXISTS, and make the restoration report its work (@xarmian)
- a3a1d58: fix(store): preserve valid item references across workspace import (#1271) (@mattfaltyn)
- 56c46ae: fix(store): propagate a collection rename into relation fields that target it (BUG-2873) (#1243) (@xarmian)
- f789eaa: fix(store): protect items.slug against the import path, and make restoration atomic (@xarmian)
- 46c55e3: fix(store): re-assert triggers after a table rebuild, and correct two inherited classifications (@xarmian)
- 9ecc59a: fix(store): refuse a schema with trailing content instead of truncating it (BUG-2873) (@xarmian)
- 46a551a: fix(store): refuse an outbox row on its STORED size, not its Go size (BUG-2827) (@xarmian)
- 5872647: fix(store): restore triggers after every migration, gated on its own (@xarmian)
- 978af70: fix(store): sweep by pattern rather than a name list, and quote the dropped identifier (@xarmian)
- 5d2ae30: fix(store): sweep the attribution-column class, and treat a stray trigger as unhealthy (@xarmian)
- d86a499: fix(store): the SQLSTATE extractor indexed one string and sliced another (BUG-2810) (@xarmian)
- 0363c13: fix(store,cli): the oracle failed open, and it over-refuses one column (BUG-2810) (@xarmian)
- 21b4d8d: fix(store,links): remove both quadratics in the item rename cascade and bound it (BUG-2804) (#1224) (@xarmian)
- 5b12d5e: fix(store,server): bound the outbox's unbounded reads and writes (BUG-2827) (@xarmian)
- 74c786d: fix(store,server): close codex round 2 — the wrapper must mirror its base (@xarmian)
- cc8bf88: fix(store,server): close codex round 3 — inspect what is actually bound (@xarmian)
- 213142c: fix(store,server): honest refusal figures and a throttled oversized scan (BUG-2827) (@xarmian)
- 49bd342: fix(store,server,cli): three defects from codex round 1 (BUG-2810) (@xarmian)
- ef792bd: fix(store,server,textguard): close codex round 1 on S1 — seven findings (@xarmian)
- f83de5b: fix(store,textguard): close codex round 4 — stop chasing shapes, and correct a claim (@xarmian)
- 9df2da2: fix(web): a background children refresh keeps its row nodes, so clicks are not swallowed (BUG-2871) (#1311) (@xarmian)
- 9e121bd: fix(web): a board lane writes a value the schema can hold, not the lane key (BUG-3057) (#1353) (@xarmian)
- 3e5796e: fix(web): a card status chip writes status, not the group field (BUG-3068) (#1357) (@xarmian)
- 552230b: fix(web): a cleared picker owes a refresh even on an unchanged scope (TASK-2877) (@xarmian)
- 7b32e57: fix(web): a dropped workspace needs an identity signal, not an epoch (TASK-2877) (@xarmian)
- 761e6e2: fix(web): a failed cold search is not evidence that nothing matched (TASK-2877) (@xarmian)
- 8f6c62e: fix(web): a failed links refresh keeps the links it has, so a click cannot be lost to it (BUG-2871) (#1315) (@xarmian)
- 5db3425: fix(web): a field value that is not a string no longer takes the view down (BUG-3041) (#1345) (@xarmian)
- 1c15462: fix(web): a list item grouped by 0 or false no longer vanishes (BUG-3053) (#1348) (@xarmian)
- 9cdd580: fix(web): a reconcile loop cannot report catch-up for a verdict a resync overtook (TASK-2909) (#1266) (@xarmian)
- 09f80b3: fix(web): a renamed target collection must not read as lost data (TASK-2868) (@xarmian)
- 268594e: fix(web): a scope change re-queries a server-sourced picker too (TASK-2877) (@xarmian)
- 60fe815: fix(web): a scope refresh stays owed until a run serves it (TASK-2877) (@xarmian)
- cab78e7: fix(web): a snapshot cannot reinstate a row whose eviction the cache already consumed (TASK-2920) (#1273) (@xarmian)
- 09a1838: fix(web): a submit no longer erases what arrived mid-flight; empty states wait for the last page (IDEA-2843) (@xarmian)
- 5032bac: fix(web): a tab may only write to the durable cache under the scope that cache advertises (TASK-2922) (#1282) (@xarmian)
- 74448dd: fix(web): a typed edit survives the previous keystroke echoing back (BUG-3039) (#1347) (@xarmian)
- e0f54c5: fix(web): a workspace you cannot reach is asked about once, not once per tick (BUG-2983) (#1354) (@xarmian)
- 6a335dd: fix(web): absence is only evidence from a settled index; fence the error toast (TASK-2877) (@xarmian)
- 17ec291: fix(web): align the type-select to the picker input, not to the whole picker (TASK-2862) (@xarmian)
- 1a76531: fix(web): capture the pane-follow target at keypress, not when the timer fires (BUG-2848) (@xarmian)
- 8d7fdc2: fix(web): consult the error state everywhere success was assumed (IDEA-2843) (@xarmian)
- ff44e91: fix(web): count the 401 drop; a truncated page is not an answer (TASK-2877) (@xarmian)
- 4b0a818: fix(web): drop stale rows on an index reset; re-filter on a late exclusion set (TASK-2862) (@xarmian)
- 5dffd73: fix(web): fence the create against cancel and against a workspace switch (TASK-2877) (@xarmian)
- 83e4abc: fix(web): fence the in-flight create; ask the index, not the ranking (TASK-2877) (@xarmian)
- 5782042: fix(web): four singleton stores kept the previous user's data across an identity change (BUG-3005) (#1325) (@xarmian)
- 4ebad40: fix(web): gate the composer's item identity during A→B navigation (IDEA-2843) (@xarmian)
- 6f7c09a: fix(web): judge a relation's collection only when the list and index agree (TASK-2868) (@xarmian)
- 3de173a: fix(web): keep an open picker current with the local index (TASK-2862) (@xarmian)
- b82c689: fix(web): pagination honours the caller's filter; the host's Load more is styled (IDEA-2843) (@xarmian)
- abe8d93: fix(web): per-view titles, counts and empty states; a failed quote is not silent (IDEA-2843) (@xarmian)
- 3c63bfb: fix(web): re-list a scoped picker after hydration; correct the Escape prose (TASK-2862) (@xarmian)
- af24997: fix(web): re-resolve the follow target by id at fire time (BUG-2848) (@xarmian)
- c516871: fix(web): read page completeness from the page, not from
total(TASK-2877) (@xarmian) - 03ec276: fix(web): refresh on the search epoch, not the workspace cursor (TASK-2862) (@xarmian)
- 5079a53: fix(web): resolve a relation by id in its own collection; collapse the picker (TASK-2868) (@xarmian)
- 42c0f4d: fix(web): scope the pane focus-ring suppression to mobile, where focus is programmatic (TASK-2245 / C119) (#1305) (@xarmian)
- 06ccabd: fix(web): settings permissions are sticky, so the owner-only tab survives the /me window (BUG-2978) (#1307) (@xarmian)
- d2de053: fix(web): six create paths ask the status field's type before defaulting it (BUG-3078) (#1360) (@xarmian)
- f7bb735: fix(web): state the cold rule positively; catch the epoch reset (TASK-2877) (@xarmian)
- 721a638: fix(web): table, share and card surfaces render a relation, not its stored id (BUG-3016) (#1352) (@xarmian)
- a03c6d6: fix(web): ten surfaces ask a field type before printing its value (BUG-3067) (#1358) (@xarmian)
- 0224053: fix(web): the board lane's bulk move offers only destinations the field can hold (BUG-3074) (#1359) (@xarmian)
- d387d54: fix(web): the cold path pins its cursor to the snapshot and owns the replay (IDEA-2924) (#1280) (@xarmian)
- e426f8c: fix(web): the durable item cache's cursor never moves backward, and a refused batch stamps no epoch (TASK-2906) (#1264) (@xarmian)
- 6b9e7bc: fix(web): the index owns its signals — one reconcile loop, driven by the workspace layout (TASK-2921) (#1278) (@xarmian)
- 74d6564: fix(web): the picker's collection scope is a tracked input (TASK-2877) (@xarmian)
- e864a43: fix(web): the reconcile token outlives the state it counts, so a verdict cannot cross a reset (IDEA-2913) (#1269) (@xarmian)
- 7a7e9d6: fix(web): the selection toolbar is a row again (IDEA-2843) (@xarmian)
- e92f6f2: fix(web): the sidebar footer's Settings label wrapped once the GitHub link joined the row (BUG-2844) (@xarmian)
- bba2da6: fix(web): the user menu's Sign out is inside the panel, not below its edge (BUG-2985) (#1355) (@xarmian)
- 5c0d8e9: fix(web): three codex round-1 findings on the timeline split (IDEA-2843) (@xarmian)
- 89f55ba: fix(web): title parts are route-scoped, replacing the clear that raced them (TASK-2245 / C118) (@xarmian)
- 6d77856: fix(web): unknown membership is not a denial — stop dropping it on a repeat resolve (TASK-2988) (#1313) (@xarmian)
- 9839884: fix(web): user-scoped state and side effects must expire when the user changes (BUG-2990 / BUG-2991 / BUG-3004) (#1321) (@xarmian)
- b3ca3a9: fix(web): vitest refuses a project it cannot run instead of dropping it (BUG-3045) (#1346) (@xarmian)
- 22fce21: fix(web): workspace identity recovers after a failed cold load, on the condition rather than a signal (TASK-2200) (#1284) (@xarmian)
- a602460: fix(web): wrap the admin console tab strip instead of hiding its scrollbar (TASK-2979) (#1308) (@xarmian)
- 0b16be4: fix(web): wrap the settings tab bar so no tab is hidden at phone width (TASK-2245 / C82) (#1306) (@xarmian)
- 367aae8: fix: one prefix grammar, and the ref parser widens to it (BUG-2943) (#1286) (@xarmian)
Performance
- 6f8b6eb: perf(store): clone Postgres test databases from a migrated template (TASK-2900) (#1260) (@xarmian)
- 3c93472: perf(store): run the store suite in parallel (TASK-2900 deliverable 3) (#1263) (@xarmian)
Refactors
- 1de4fd4: refactor(mcp): one canonical view, one conflict check (BUG-2850) (@xarmian)
- d0644a6: refactor(textguard): extract the one decoded-NUL predicate and its corpus (DOC-2823 S1) (@xarmian)
- f932469: refactor(web): extract TimelineEntryList from ItemTimeline, no behaviour change (IDEA-2843) (@xarmian)
- c13887a: refactor(web): the keyed single-flight loader has one definition, not two (TASK-2947) (@xarmian)
Other changes
- d6771ab: IDEA-2886: state the drop-vs-refuse rule once, and route all three sites through it (#1256) (@xarmian)
- 47cc106: IDEA-2893 + IDEA-2894: record the accepted carry disclosure, and make the drop-reason mapper testable (#1254) (@xarmian)
- 07b2e43: TASK-2869 (U2b): the preflight names a relation's target collection, and the copy dialog scopes its picker to the destination (#1258) (@xarmian)
- 99ec711: chore(ci)(deps): bump anchore/sbom-action/download-syft (#1222) (@dependabot[bot])
- 31d11e7: chore(deps)(deps): bump github.com/mark3labs/mcp-go from 0.58.0 to 1.0.0 (#1275) (@dependabot[bot])
- b7235a1: chore(deps)(deps): bump the go-minor-and-patch group across 1 directory with 6 updates (#1301) (@dependabot[bot])
- 459ba37: chore(deps)(deps): bump the npm-minor-and-patch group (#1223) (@dependabot[bot])
- bc543a1: chore(deps)(deps): bump the npm-minor-and-patch group (#1276) (@dependabot[bot])
- d89d624: chore(deps)(deps-dev): bump vitest from 4.1.11 to 5.0.0 in /web (#1277) (@dependabot[bot])
- 0900be6: chore(deps): bump golang.org/x/crypto to v0.56.0 (BUG-2851) (@xarmian)
- c141c73: chore(deps): bump google.golang.org/grpc 1.83.0 -> 1.83.2 (BUG-2986) (#1322) (@xarmian)
- 249a8f8: chore(nix): heal vendorHash for the module set in 31d11e7 (TASK-2954) (@github-actions[bot])
- 5ce1103: chore(nix): heal vendorHash for the module set in b7235a1 (TASK-2954, BUG-2974) (@xarmian)
- d9ea533: chore(nix): heal vendorHash for the module set in c141c73 (TASK-2954) (@github-actions[bot])
- 1347ca5: chore(nix): version 0.16.0 for the release-0.16.x cut (TASK-3070) (@xarmian)
- 49d1bfc: ci(nix): a Go bump's Nix check is green when the build passes, and main heals its own vendorHash (TASK-2954) (#1292) (@xarmian)
- a1716d8: ci(web): decide the npm audit gate from the report, not the exit code, and run it last (BUG-2881) (#1247) (@xarmian)
- b2c303c: docs(links,store,models): cite markdown.ts by symbol, and check it (BUG-2832) (@xarmian)
- 80be76a: docs(mcp): the detectFieldConflicts header stated the pre-round-14 reach (BUG-2850) (@xarmian)
- 793fad9: docs(mcp): the reason given for the protocol restriction was false — replace it with the true one (TASK-2977) (#1312) (@xarmian)
- f54a0e4: docs(store,server): four comments and one log line that had stopped being true (BUG-2827) (@xarmian)
- ded64ce: docs(web): record why three races are deliberately not fenced (TASK-2877) (@xarmian)
- 90dadec: docs(web): the cold-path pin heals one of the eviction floor's four doors, not all four (TASK-2939) (#1290) (@xarmian)
- 57caa7f: docs,test(store): correct five comments and strengthen the shrink fixture (BUG-2827) (@xarmian)
- a2bdd90: docs,test(web): correct two claims the mutation matrix refuted (IDEA-2843) (@xarmian)
- 809249a: test(e2e): assert the ctrl-click tab through CDP, not through a page event (BUG-2993) (#1350) (@xarmian)
- bb62cfd: test(mcp): derive the conflict property's population from the declared schema (BUG-2850) (@xarmian)
- 49e533d: test(mcp,cli): pin same-name duplicate precedence on both doors (BUG-2850) (@xarmian)
- b451fb5: test(server): bind the coercion to its call sites, and enforce copy/preflight agreement (BUG-2850) (@xarmian)
- 7f25283: test(server): pin the last three coercion call sites (BUG-2850) (@xarmian)
- 696b477: test(server): the mobile shells' unversioned contracts fail CI when they move (TASK-2053) (#1294) (@xarmian)
- 976fff4: test(store): measure the write guard's cost, and report it at the width it holds (@xarmian)
- 43c33d1: test(web): make the picker's fence and consumption guards actually discriminate (TASK-2862) (@xarmian)