Skip to content

Pad v0.12.0

Choose a tag to compare

@github-actions github-actions released this 11 Aug 18:22
· 300 commits to main since this release

Highlights

Pad v0.12.0 is the attachments release. A unified viewer replaces the old image lightbox and now opens any attachment, with real zoom, touch gestures, and a proper modal contract — and the security work underneath it closes a cross-workspace metadata leak. Cross-workspace item copy/move, a violet UI refresh, and a Nix install path round it out.

The unified attachment viewer

Attachments now have one viewer instead of an images-only lightbox and a separate details dialog. Click any attachment — image, PDF, archive, or an unrecognized blob — and the same surface opens, with a metadata header (filename, type, size), a toolbar of actions, and a file-type icon fallback when there are no displayable bytes (TASK-2474 / TASK-2475 / TASK-2476). Images get real zoom: wheel and ctrl/cmd-wheel anchored at the pointer, double-click to toggle fit, drag-to-pan, and keyboard zoom, all built on a pure transform module rather than CSS guesswork (TASK-2454 / TASK-2455 / TASK-2457 / TASK-2458). Desktop loads a thumbnail first and promotes to the original; mobile holds back until you tap, then fetches full bytes when you zoom past fit, so a phone on cell data isn't pulling multi-megabyte originals to render a 200px tile (TASK-2459 / TASK-2460). On phones the viewer takes over as a bottom sheet with one-finger pan, pinch zoom, and double-tap, with touch-action: none so the browser doesn't steal the gesture (TASK-2492 / TASK-2517 / TASK-2518).

Under it, six separate producers were repointed onto a single surface-open channel and the old panel retired in one atomic cutover, so every entry point — strip tiles, editor chips, inline body images, the storage tab — now agrees about what "open this attachment" means (TASK-2485 through TASK-2490). Inline body images are keyboard-activatable and route through the same viewer instead of a hand-rolled dialog (TASK-2432 / TASK-2433). The viewer is a real modal: portalled, focus-trapped, backdrop-refcounted, app shell inerted, and registered on the shared escape stack so one ESC closes one layer and seven other global key and gesture owners stand down while it's frontmost (TASK-2427 / TASK-2429 / TASK-2430 / BUG-2441). Deleting the attachment you're looking at advances to the next one by identity or closes the viewer if it was the last (TASK-2477), and reopening always revalidates metadata so a stale thumbnail never survives a delete-and-reupload (TASK-2491). New below the item properties: an attachment strip with upload refresh and an in-use-aware delete confirm, replacing window.confirm (TASK-2383 / TASK-2384 / TASK-2385 / TASK-2425).

Attachment security hardening

Two authorization defects and the data invariant they rested on are fixed; self-hosted multi-user and Pad Cloud deployments should upgrade. An attachment's item_id could point at an item in a different workspace, and the read path leaked metadata across that boundary while simultaneously locking legitimate grant-based guests out (BUG-2387, high). The invariant is now fixed at the source: one effective item_id is parsed from both the query-string and multipart-form channels, resolved within the request workspace, and persisted as the canonical UUID — never the caller's string (TASK-2400). Every query that trusted it is workspace-scoped (TASK-2399 / TASK-2403). The flat role gate on the blob path is replaced with real item-visibility resolution, closing a variant IDOR where authorizing a parent and serving its child defeated the check (BUG-2386, TASK-2401). Archived and soft-deleted parents now have defined semantics on the read, transform, and derivation paths (TASK-2402 / TASK-2404), cross-workspace copy authorizes each attachment reference it carries (BUG-2407), and unknown or disallowed stored MIME types are force-downloaded rather than rendered inline (BUG-2413). The multipart spool is also cleaned up on both success and failure.

Cross-workspace item copy and move

Items can move between workspaces. pad item copy <ref> --to-workspace <slug> --collection <slug> copies an item, and --archive-source makes it a move; the web UI has the same flow under the item pane's ⋯ → "Copy or move to workspace…" (TASK-2355 / TASK-2366). Because destination collections have different schemas, a preflight endpoint tells you exactly what would carry, what would drop, and what needs a value before anything mutates — --dry-run on the CLI renders it, and the copy refuses rather than guessing when a required destination field has no source (TASK-2364). Attachment handling is explicit: pad-attachment: refs that resolve to a live attachment in the source workspace are cloned and repointed at the clones, while foreign, soft-deleted, or dangling ids are left literal and counted as unresolvable (TASK-2354). [[wiki-links]] are deliberately not rewritten — they re-resolve in the destination, which the CLI and the dialog both warn about. Moves leave a provenance record and the source item exposes an ACL-gated moved-to pointer, rendered as a banner so a stale bookmark tells you where the item went (TASK-2356 / TASK-2359). The copy has no idempotency key, so neither the CLI nor the clients ever retry it automatically.

UI refresh — violet theme and shared primitives

The web UI got a systematic pass. A violet accent replaces the old palette, built on a token foundation with a defined neutral scale, card tokens, and radius scale, with AA-contrast text on filled surfaces in both themes (TASK-2291). Five shared primitives were extracted and adopted across the app rather than left as one-offs: Chip with a canonical field-color util (48 badge sites), Button (95 sites), Menu/MenuItem with escape-stack, portalling, and pointerdown dismissal, PageHeader, and a generic EmptyState (15 pages) (TASK-2292). Board and card anatomy were reworked — lane accents, card tokens, a violet focus ring — and the collection toolbar consolidated into a View menu with saved views plus sort/filter icons and a collection ⋯ menu (TASK-2293). The item pane now has tabs (Details / Relationships / Activity / Versions) and the editor never unmounts when you switch between them, so a live collab session survives tab changes (TASK-2294). Two table-view bugs are fixed: rows collapsing into vertical stacks in current Chromium (subgrid + content-visibility), and status color dropping out entirely — both of which also affected public share links (TASK-2208 / TASK-2213).

Nix, CLI, and platform

Pad is packaged as a Nix flake, so nix run github:PerpetualSoftware/pad/release runs the latest verified release without installing anything — the release branch pointer tracks the most recent release that passed verification, never a broken main. A Nix CI job runs on every push and PR, which makes vendorHash drift a merge-blocking tripwire rather than something a user discovers. The list commands gained markdown output (--format markdown), useful for pasting a query straight into a doc or an agent prompt. Item headers show a 🌳 done/total children jump-badge (TASK-2138), and the mobile pass collapsed the "Live" badge into the action bar and made lane inline-create open the split pane on desktop while staying put on mobile (IDEA-2297 / IDEA-2298).

Install

brew install perpetualsoftware/tap/pad
# or run the latest verified release with Nix, no install
nix run github:PerpetualSoftware/pad/release
# or pull the container image
docker pull ghcr.io/perpetualsoftware/pad:0.12.0

Verifying

Every archive is signed with cosign (keyless / Sigstore OIDC) and ships an SBOM + SLSA build provenance. The checksums signature ships as a single Sigstore bundle (checksums.txt.sigstore.json) — verify with:

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

Changelog

Features

  • 4fb946c: feat(a11y): add refcounted viewer backdrop manager (TASK-2427) (@xarmian)
  • 7cfe50d: feat(a11y): defer global key and gesture owners to a frontmost viewer (TASK-2430) (@xarmian)
  • 1385243: feat(a11y): give the attachment viewer a real modal contract (TASK-2429) (@xarmian)
  • 092498d: feat(a11y): make inline body images keyboard-activatable (TASK-2432) (@xarmian)
  • 7826866: feat(attachments): DR-5b classification and desktop thumb-then-original loading (TASK-2459) (@xarmian)
  • e79a9bb: feat(attachments): DR-5b mobile policy — tap-to-load and fetch-on-zoom-past-fit (TASK-2460) (@xarmian)
  • af055a9: feat(attachments): Lightbox admits any attachment — file route and reclassification (TASK-2486) (@xarmian)
  • 4f57a97: feat(attachments): action icons — MenuItem snippet path, actions.ts to SVG ids (TASK-2472) (@xarmian)
  • 27de622: feat(attachments): add pure zoom/pan math module (TASK-2454) (@xarmian)
  • 884ddc5: feat(attachments): add viewer open channel and per-host viewer host (TASK-2428) (@xarmian)
  • 72a7491: feat(attachments): all six producers emit the surface channel (TASK-2489) (@xarmian)
  • 44ee5ad: feat(attachments): always-revalidate-on-open via per-open nonce (TASK-2491) (@xarmian)
  • b8909be: feat(attachments): an options panel for files (TASK-2423) (@xarmian)
  • 03e0edb: feat(attachments): complete the MIME matrix and address fence (TASK-2434) (@xarmian)
  • c6133af: feat(attachments): content slot with icon fallback and no-bytes arm (TASK-2476) (@xarmian)
  • 90c5992: feat(attachments): double-click zoom toggle and drag-to-pan (TASK-2458) (@xarmian)
  • a3f272a: feat(attachments): make the viewer open gate total across every surface (TASK-2431) (@xarmian)
  • 5ee40d7: feat(attachments): mobile phone-sheet layout for the viewer (TASK-2492) (@xarmian)
  • 1a42a6d: feat(attachments): open the options panel from tiles and chips (TASK-2424) (@xarmian)
  • ab34086: feat(attachments): pointer-anchored wheel zoom; restoration ignores viewer input (TASK-2457) (@xarmian)
  • 6047b40: feat(attachments): shared action descriptors and MenuItem anchor support (TASK-2422) (@xarmian)
  • 9dc569c: feat(attachments): the atomic cutover — one surface host, panel retires (TASK-2488) (@xarmian)
  • 51b9000: feat(attachments): thread host identity into the attachment surfaces (TASK-2421) (@xarmian)
  • 2ec6c95: feat(attachments): typed metadata result and MIME capability helpers (TASK-2420) (@xarmian)
  • e0c06b0: feat(attachments): unified surface host with three-channel bridge (TASK-2487) (@xarmian)
  • da1b3f8: feat(attachments): unified surface-open channel (TASK-2485) (@xarmian)
  • 10b99de: feat(attachments): viewer deletion subscription — advance or close by identity (TASK-2477) (@xarmian)
  • 9cf86cb: feat(attachments): viewer metadata header (TASK-2475) (@xarmian)
  • c0ec3ad: feat(attachments): viewer toolbar with host-supplied context on every mount (TASK-2474) (@xarmian)
  • e334e86: feat(attachments): wire zoom transform into Lightbox (TASK-2455) (@xarmian)
  • 1e48a7a: feat(cli): add pad item copy for cross-workspace copy and move (TASK-2366) (@xarmian)
  • 4ab7b10: feat(cli): markdown output for the list commands (@b4rk13)
  • 02b3025: feat(nix): add flake packaging for pad with CI build (@claude)
  • 5804c80: feat(server): add cross-workspace authorization helper (TASK-2358) (@xarmian)
  • 01d6409: feat(server): add cross-workspace copy dry-run preflight endpoint (TASK-2364) (@xarmian)
  • f8ff574: feat(server): add cross-workspace copy endpoint with post-commit fanout (TASK-2365) (@xarmian)
  • 1eb1c9e: feat(server): expose ACL-gated moved-to pointer on item GET (TASK-2359) (@xarmian)
  • 0fad869: feat(store): add CopyItemAcrossWorkspaces atomic orchestration (TASK-2363) (@xarmian)
  • 60dd3e1: feat(store): add attachment resolution planner for cross-workspace copy (TASK-2354) (@xarmian)
  • bf14c11: feat(store): add item_workspace_moves provenance table (TASK-2356) (@xarmian)
  • cf85e8b: feat(viewer): pointer registry + touch tap semantics (TASK-2517) (@xarmian)
  • 9d9670e: feat(viewer): touch pan, pinch, double-tap + touch-action:none (TASK-2518) (@xarmian)
  • 6422324: feat(web): Button primitive + dark text-on-fill AA — 95 sites migrated (TASK-2292) (#1020) (@xarmian)
  • a335033: feat(web): Chip primitive + canonical fieldColors util — 48 badge sites migrated (TASK-2292) (#1019) (@xarmian)
  • 01a94d9: feat(web): Menu/MenuItem primitive — 3 menus migrated, escape-stack + portal + pointerdown dismissal (TASK-2292) (#1022) (@xarmian)
  • d087c7d: feat(web): PageHeader primitive + generic EmptyState — 15 pages adopted (TASK-2292) (#1021) (@xarmian)
  • e9e114e: feat(web): TableView + share parity; fix subgrid collapse + hyphenated lane accents (TASK-2293/2208/2213) (#1024) (@xarmian)
  • bbb21ef: feat(web): add copy/preflight API client methods (TASK-2355) (@xarmian)
  • bdb6f6e: feat(web): add item attachment strip below properties (TASK-2383) (@xarmian)
  • 5d327c9: feat(web): add the cross-workspace copy dialog (TASK-2355) (@xarmian)
  • f994509: feat(web): card anatomy — Chip status/priority, card tokens, violet ring, lane accents (TASK-2293) (#1023) (@xarmian)
  • 563371e: feat(web): define missing token families + zero-change drift sweep (TASK-2291) (#1017) (@xarmian)
  • e115bb2: feat(web): delete attachments from the item strip (TASK-2384) (@xarmian)
  • 10a5ae2: feat(web): dissolve the item action bar into a new .tab-strip wrapper (TASK-2328) (#1033) (@xarmian)
  • d04b714: feat(web): item pane tabs — Details/Relationships/Activity/Versions, editor never unmounts (TASK-2294) (#1027) (@xarmian)
  • 26c3f02: feat(web): pane action bar consolidates into the ⋯ overflow (TASK-2294 PR B) (#1029) (@xarmian)
  • e3d8061: feat(web): reconcile timeline attMeta across attachment lifecycle (TASK-2510) (@xarmian)
  • a99e71a: feat(web): refresh the attachment strip on upload (TASK-2385) (@xarmian)
  • 8aa87f2: feat(web): render the archived-source provenance banner (TASK-2355) (@xarmian)
  • c5190d6: feat(web): revalidate attachment metadata per navigation step (TASK-2512) (@xarmian)
  • 08b6d94: feat(web): revalidate attachment strip content on parent restore (TASK-2511) (@xarmian)
  • 1747054: feat(web): toolbar consolidation — View menu w/ saved views, sort/filter icons, collection ⋯ menu (TASK-2293) (#1025) (@xarmian)
  • 841a2cb: feat(web): violet retheme — accent-primary alias, neutral scale, card tokens, radius, AA text (TASK-2291) (#1018) (@xarmian)

Bug fixes

  • df74117: fix(a11y): keep Escape consumption event-scoped so one press closes one layer (BUG-2441, TASK-2448) (@xarmian)
  • 124ebf8: fix(a11y): stop a HELD Escape cascading past the viewer (TASK-2448) (@xarmian)
  • 95b6e1b: fix(attachments): a deleted inline image is inert, like the chip beside it (@xarmian)
  • 5a6625e: fix(attachments): abandon an open confirmation when permission is withdrawn (@xarmian)
  • cccb9d5: fix(attachments): bound strip buffers and surface load failures (TASK-2418) (@xarmian)
  • ba848af: fix(attachments): check restriction before the role gate on orphan delete (@xarmian)
  • 2318a17: fix(attachments): classify derived rows after authorization on delete (@xarmian)
  • 042bd7e: fix(attachments): close four review findings across the wave-A surfaces (@xarmian)
  • 7bb9c4a: fix(attachments): close the final-review findings across the feature (@xarmian)
  • bbb5a69: fix(attachments): dock-clear the viewer nav on the mobile sheet (PLAN-2392 3c-ii) (@xarmian)
  • 6670073: fix(attachments): fence delete at entry and complete the view-identity model (TASK-2418) (@xarmian)
  • 749abbf: fix(attachments): fence the panel's teardown and its deferred close (@xarmian)
  • 2501046: fix(attachments): fence transform failures and state what itemId means (@xarmian)
  • d4da83d: fix(attachments): final-pass fixes — re-clamp on same-id reload, inert error state (PLAN-2392 3b) (@xarmian)
  • d16ba34: fix(attachments): finish the gate and the rollout the last round started (@xarmian)
  • 6e2b972: fix(attachments): gate blob reads on item visibility (TASK-2401) (@xarmian)
  • 380b75e: fix(attachments): gate transform on item visibility (TASK-2402) (@xarmian)
  • 07b9a04: fix(attachments): hand off focus before a viewer control disappears (TASK-2456) (@xarmian)
  • ebe531e: fix(attachments): make the host address a reader, not written-in options (@xarmian)
  • 74589e9: fix(attachments): make the live chip a button, not a link (TASK-2424) (@xarmian)
  • 2cbce05: fix(attachments): make the surfaces of one object agree (@xarmian)
  • a40049c: fix(attachments): meet criteria 1, 3 and 5 as written (@xarmian)
  • 74ccada: fix(attachments): name the nameless, and stop a hung read looking like a wait (@xarmian)
  • c4189b1: fix(attachments): one in-app delete confirmation, everywhere (TASK-2425) (@xarmian)
  • 850d355: fix(attachments): reconcile the panel on deletion, guard the storage delete (@xarmian)
  • 3b4331d: fix(attachments): reload storage tab on workspace change (TASK-2418) (@xarmian)
  • 27b71fe: fix(attachments): resolve item_id across both upload channels (TASK-2400) (@xarmian)
  • ca0f4d2: fix(attachments): revalidate before answering an existence probe (TASK-2420) (@xarmian)
  • 90eb871: fix(attachments): skip derivation for an archived parent (TASK-2404) (@xarmian)
  • 7e85d1c: fix(attachments): viewer honors the missing metadata phase; comment refresh (PLAN-2392 3c-i) (@xarmian)
  • f900b0a: fix(cli): address review on markdown list output (@b4rk13)
  • 20b0619: fix(cli): surface actionable errors for cloud-mode setup failures (@danfinn5)
  • e12feb4: fix(copy): authorize attachment references in cross-workspace copy (TASK-2408) (@xarmian)
  • a94f2d3: fix(deps): bump otel to v1.42.0 — clears GO-2026-5506 + GO-2026-5158, un-reds main CI (#1026) (@xarmian)
  • 0a54728: fix(deps): keep @dagrejs/dagre pinned at 3.0.0 (@xarmian)
  • 8bc3c5f: fix(e2e): graph tests open the drawer via the pane ⋯ overflow (missed in #1029 — only capstone/host were re-run locally) (#1030) (@xarmian)
  • e08901d: fix(e2e): resolve imported attachment via content reference, not list page 1 (BUG-2504) (#1075) (@xarmian)
  • 7b46894: fix(e2e): silence cross-actor SSE creation toasts suite-wide (BUG-2334) (@xarmian)
  • f8ecb69: fix(editor): deliver server capabilities to the image NodeView (BUG-2426, TASK-2435) (@xarmian)
  • 0d31bae: fix(nix): refresh vendorHash for current main's go.sum (@xarmian)
  • a62a50d: fix(nix): refresh vendorHash for go-minor-and-patch group bump (@xarmian)
  • a65bfcc: fix(nix): skip DNS-dependent webhook validation subtests in checkPhase (@claude)
  • 3bd6244: fix(server): force-download unknown and disallowed stored MIME (BUG-2413) (@xarmian)
  • cfc83e8: fix(server): report partial and legacy relationships in the copy dry-run (TASK-2369) (@xarmian)
  • 2b9da94: fix(store): classify unique violations as expected copy rejections per final review (@xarmian)
  • 66fa464: fix(store): distinguish SQLite lock timeout from a real deadlock per final review (@xarmian)
  • c783d36: fix(store): make migration 077 constraint-equivalent to 055 per final review (@xarmian)
  • eae42b8: fix(store): scope attachment list JOINs by workspace (TASK-2399) (@xarmian)
  • 9ad7181: fix(store): workspace-scope the item-grant lookup (TASK-2403) (@xarmian)
  • 837035e: fix(viewer): disarm failed promotion, gate degrade, rebase pinch on flip (PLAN-2392 3d fix round) (@xarmian)
  • c676e08: fix(web): Phase 5 sweep stragglers — home priority chips, count pill, activity from-value legibility (TASK-2295) (#1031) (@xarmian)
  • 83e5958: fix(web): classify three more guaranteed pre-write refusals (TASK-2355) (@xarmian)
  • fd7c77c: fix(web): collapse the mobile "Live" badge into the action bar (IDEA-2297) (#1038) (@xarmian)
  • 53dc0b7: fix(web): delete confirmation becomes an in-menu sub-view (TASK-2327) (#1032) (@xarmian)
  • 3d19101: fix(web): fence ordinary attachment loads against in-flight deletes (@xarmian)
  • a6b48c7: fix(web): lane inline-create opens the split pane on desktop, stays put on mobile (IDEA-2298) (#1036) (@xarmian)
  • 059cbcd: fix(web): pane tabs activate on pointerdown (focus-follows click-swallow, CI-caught) (#1028) (@xarmian)
  • 33598bc: fix(web): supersede an in-progress confirm when overrides change (TASK-2355) (@xarmian)
  • e97a13e: fix(web): untrack the copy dialog's open/close reset (BUG-2379) (@xarmian)

Refactors

  • 1da9610: refactor(attachments): centralize parent resolution, close orphan-read and delete-denial gaps (@xarmian)
  • 6f8105b: refactor(attachments): consolidate icon helpers onto an SVG set (TASK-2417) (@xarmian)
  • 9c40a5f: refactor(attachments): extract surface metadata + delete-confirm machines (TASK-2473) (@xarmian)
  • 783e9ef: refactor(attachments): extract the view-identity fence into one module (@xarmian)
  • 37dd850: refactor(attachments): let the descriptors own the preview predicate per review (@xarmian)
  • af2a6ed: refactor(attachments): retire the two legacy attachment channels (TASK-2490) (@xarmian)
  • 30fa9fc: refactor(attachments): route inline images through the unified viewer (TASK-2433) (@xarmian)
  • 4b2fbe5: refactor(attachments): separate view identity from request generation per final review (@xarmian)
  • 98c638f: refactor(server): extract resolveAuthorizedCopy shared by preflight and copy (TASK-2370) (@xarmian)
  • d9d96b8: refactor(store): delete two unused item-workspace-move accessors (TASK-2374) (@xarmian)
  • dbede59: refactor(store): extract tx-taking item creation helper (TASK-2362) (@xarmian)
  • c6ebe5a: refactor(store): unify the collection column list and scan (TASK-2368) (@xarmian)
  • 2dfce6c: refactor(web): centralize attachment deletion + upload mapping per final review (@xarmian)

Other changes

  • 0f69e7b: chore(ci)(deps): bump actions/cache from 5.0.5 to 6.1.0 (@dependabot[bot])
  • 1dafa37: chore(ci)(deps): bump actions/checkout from 6.0.2 to 7.0.1 (@dependabot[bot])
  • 2ab5b10: chore(ci)(deps): bump actions/setup-go from 6.5.0 to 7.0.0 (@dependabot[bot])
  • b5d0d5a: chore(ci)(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (@dependabot[bot])
  • 38e1042: chore(ci)(deps): bump docker/login-action (@dependabot[bot])
  • ca45539: chore(deps)(deps): bump the go-minor-and-patch group across 1 directory with 13 updates (@dependabot[bot])
  • 3f0b390: chore(deps)(deps): bump the npm-minor-and-patch group across 1 directory with 24 updates (@dependabot[bot])
  • cdb495d: chore(deps): bump google.golang.org/grpc to v1.82.1 for GO-2026-6061 (BUG-2361) (#1050) (@xarmian)
  • a66c9ab: chore(nix): bump version to 0.12.0 (@xarmian)
  • 71c1820: ci(nix): pin nix-installer-action to the actual v22 commit (@xarmian)
  • b90e7ed: docs(attachments): record the lock-held pool I/O hazard at the call site (BUG-2409) (@xarmian)
  • 5044e22: docs(cli): document copy content semantics (TASK-2355) (@xarmian)
  • f15ba86: docs(server): correct the cross-workspace authz re-check contract per final review (@xarmian)
  • 74813fc: revert(web): restore the pre-TASK-2328 item action bar, then make it fit (PLAN-2326 overturned) (#1035) (@xarmian)
  • a593407: test(attachments): browser proof for the 3c-i surface chrome (TASK-2484) (@xarmian)
  • 8726123: test(attachments): browser proof for viewer zoom — desktop and mobile (TASK-2461) (@xarmian)
  • b253a2b: test(attachments): cover the wiring the unit suites structurally cannot (@xarmian)
  • 222c596: test(e2e): add BLOG-2289 v0.11 pane screenshot capture block (#1016) (@xarmian)
  • 228f993: test(e2e): prove the viewer's modal contract in a browser (TASK-2436) (@xarmian)
  • 9c91071: test(e2e): reconcile attachment e2e with the converged surface (TASK-2493) (@xarmian)
  • 3bbd326: test(store): make the copy concurrency and attachment assertions bite (TASK-2372) (@xarmian)
  • e9af504: test(store): pin DR-15 attachment clone/move/bundle semantics (TASK-2478) (@xarmian)
  • 84eef5d: test(viewer): CDP mobile touch-gesture proof + device checklist (TASK-2519) (@xarmian)
  • 885871a: test(web): browser-prove attachment lifecycle completeness (TASK-2514) (@xarmian)
  • fbfbfcf: test(web): e2e coverage for the copy/move dialog (TASK-2355) (@xarmian)
  • ffaba63: test(web): pin attachment open-set mutation contracts (TASK-2513) (@xarmian)
  • 7bb76a5: test(webhooks): use a literal IP instead of example.com in two more spots (@claude)