diff --git a/.github/skills/xlf-strings/SKILL.md b/.github/skills/xlf-strings/SKILL.md
index c9c36104a226..ab88d76dcbfd 100644
--- a/.github/skills/xlf-strings/SKILL.md
+++ b/.github/skills/xlf-strings/SKILL.md
@@ -46,6 +46,8 @@ After the `ID: ...` line, add a **second ``** whenever a tran
The note should state: what UI element it labels, where it appears in the UI, and any constraints (e.g. "appears mid-sentence, should be lowercase", "step N of M in X instructions", "'Bloom' is a product name and must not be translated", "{0} is replaced with a count").
+**Never put a double hyphen (`--`) inside `` text.** L10NSharp's XLIFF reader routes note content through XML-comment parsing, and `--` in a comment is illegal XML — Bloom then crashes at startup with "An XML comment cannot contain '--'" while loading localization (found 8 Jul 2026; it killed every launch). Use a period, semicolon, or single hyphen instead. (`--` in `` text is fine.)
+
Example with context note:
```xml
diff --git a/Design/CloudTeamCollections.md b/Design/CloudTeamCollections.md
new file mode 100644
index 000000000000..3a4cdefe8e62
--- /dev/null
+++ b/Design/CloudTeamCollections.md
@@ -0,0 +1,180 @@
+# Cloud Team Collections: S3 + Supabase design
+
+Status: **design complete, implementation not started** (2 July 2026).
+Interactive review version of this document (with diagrams and the full narrative):
+https://claude.ai/code/artifact/14157962-d8e4-4c3f-afa7-34c49ed29e1a
+Work breakdown: [CloudTeamCollections/IMPLEMENTATION.md](CloudTeamCollections/IMPLEMENTATION.md) ·
+API contracts: [CloudTeamCollections/CONTRACTS.md](CloudTeamCollections/CONTRACTS.md)
+
+## Motivation
+
+Team Collections today wraps a shared Dropbox/LAN folder. Chronic problems: Dropbox is hard
+and expensive for our users to set up; Bloom can never truly know sync status; and a
+third-party sync agent underneath us creates a long tail of races (conflicted-copy files,
+two-phase delivery, partial downloads that look corrupt) that the code and tests spend
+enormous effort taming. Replacement: a central SIL-hosted backend — Supabase Postgres as the
+authoritative database (locks, membership, current-version manifests, history events), one S3
+bucket with a prefix per collection for book content, and sharing via an approved-accounts
+model. The check-in/check-out editorial model is preserved.
+
+## Requirements (decided)
+
+1. Zero third-party setup for users; only a Bloom (BloomLibrary) sign-in.
+2. Central SIL-hosted S3; per-collection prefixes; access brokered per-operation (STS).
+3. Check-in/check-out preserved; offline editing of checked-out books always works.
+4. Identity = BloomLibrary Firebase account (real auth, not self-declared email).
+5. Sharing v1 = **approved accounts**: admin lists emails (+ role Admin/Member); an approved
+ person signs in anywhere, asks "Get my Team Collections", pulls a collection down.
+ Invite links/codes, `bloom://` deep links, landing page, Mailgun emails: all deferred to v2.
+6. **Send/Receive model** (modal in v1): explicit transfers with progress; no ambient content
+ copying. Metadata (locks, version numbers) syncs continuously and cheaply; the UI always
+ knows "checked out to Sara" / "v12 exists, you have v10" without moving bytes.
+7. No secondary local repo copy. Transfers stage in temp dirs and swap atomically per book —
+ never new HTML with stale images.
+8. Per-file delta transfers, SHA-256 verified, both directions.
+9. **No content retention**: replaced files are not kept anywhere; no restore. The history
+ *log* (who/when/comment/incidents) is kept — it is metadata. S3 object versioning stays ON
+ purely as a transactional safety device (crashed Send harmless; torn reads impossible),
+ with noncurrent versions auto-deleted after ~7 days.
+10. Work is never silently lost: whenever the repo must win over local work, that work is
+ saved as a `.bloomSource` in a local **Lost & Found** folder AND recorded as a server-side
+ incident event admins can see.
+11. Coexists indefinitely with folder/Dropbox TC (second backend behind the same abstraction).
+ No auto-migration in v1: un-team (delete link file), then enable cloud sharing.
+12. Subscriptions untouched: existing client-side gate only; the server has no tier/quota logic.
+13. Experimental flag until GA (plus the existing feature gate), like the current TC.
+
+## Architecture
+
+- **Bloom desktop (C#)**: `CloudTeamCollection` — second subclass of the existing abstract
+ `TeamCollection` (src/BloomExe/TeamCollection/TeamCollection.cs) — plus support classes
+ (`CloudCollectionClient`, `CloudRepoCache`, `CloudBookTransfer`, `BookVersionManifest`,
+ `CloudCollectionMonitor`, `CloudAuth`, `CloudJoinFlow`), a new `SharingApi`, and reuse of
+ `BloomS3Client` (session credentials + TransferUtility), the WebSocket progress harness, and
+ nearly all existing TC UI.
+- **Supabase** (schema `tc`): tables `collections`, `members` (the approved-accounts table;
+ unclaimed rows have null user_id), `books` (with the authoritative lock columns and
+ `deleted_at` tombstone), `versions` (metadata row per check-in), `version_files` (the
+ CURRENT manifest: path → sha256, size, s3_version_id; superseded rows pruned at commit),
+ `collection_file_groups`/`collection_group_files`, `color_palette_entries` (union merge =
+ `insert … on conflict do nothing`), `events` (history + realtime source + polling cursor;
+ numeric types match `BookHistoryEventType`, extended with incident types), and
+ `checkin_transactions`. RLS on everything; no direct writes to books/versions — state
+ transitions go through RPCs/edge functions.
+- **S3**: bucket per environment (`bloom-teams-production`/`-sandbox`), versioning ON,
+ lifecycle = abort-multipart 7d + expire noncurrent versions ~7d. Layout:
+ `tc/{collectionId}/books/{bookInstanceId}/{relativePath}` (+ `.manifest.json` current-state
+ backup per book; `tc/{cid}/collectionFiles/{group}/...`). Folder keyed by instance id →
+ rename is a DB row update. Existing buckets/credentials for publishing are untouched; cloud
+ collections never use embedded keys.
+- **Access brokering**: STS temporary credentials from edge functions, scoped by inline
+ session policy (write: the one book being sent, only while its transaction is open; read:
+ the collection prefix incl. `GetObjectVersion`). Uploads carry `x-amz-checksum-sha256`;
+ checkin-finish verifies before commit.
+- **Checkout** = conditional UPDATE (`WHERE locked_by IS NULL OR locked_by = me`) — race-free.
+ Locks never auto-expire; "Force Unlock" (label decided) is a distinct audited RPC.
+- **Send** = checkin-start (diff manifest → transaction + changed paths + STS creds) →
+ parallel PUT changed files → checkin-finish (verify, one DB tx: version row + manifest +
+ lock release + events; the commit is the atomicity point). Crash mid-Send: nothing
+ committed, resumable 48h, no partial state visible ever.
+- **New books** (first-class): checkin-start with `bookId:null` creates the row locked to
+ caller **with no current version — invisible to teammates** until first commit. Crash → no
+ phantom; expired transaction reaps. Same-name race → NameConflict → existing "name2"
+ resolution; duplicate instance id → existing id-conflict flow.
+- **Receive** = `get_collection_state(since)` → reuse of the existing `SyncAtStartup`
+ reconciliation → download changed files by pinned (path, s3VersionId) into temp → atomic
+ swap per book.
+- **Realtime**: one private broadcast channel per collection driven by an events-table
+ trigger; clients keep a `last_seen_event_id` cursor; `get_changes(since)` is both reconnect
+ catch-up and the 60s polling fallback (polling ships first; realtime is an optimization).
+- **Auth** (DECIDED 8 Jul 2026: **Option A** — Supabase third-party Firebase auth; brief was
+ grounded in the BloomLibrary2 + bloom-parse-server code): the login page forwards the
+ Firebase ID+refresh tokens it already holds (~5 lines in BloomLibrary2 `src/editor.ts`),
+ plus one NEW small Firebase Admin cloud function adding the static `role:"authenticated"`
+ claim (+ backfill; no claims infra exists today). Rejected alternatives: B = exchange the
+ legacy Parse session token (welds us to the server being decommissioned); C = hand-validate
+ Firebase JWTs per the stale `bloom-parse-server/supabase/` docs. `CloudAuth` isolates the
+ choice. Claiming an approval requires `email_verified` (all BloomLibrary accounts are
+ verified — the existing parse adapter already enforces this). Go-live steps:
+ `CloudTeamCollections/GOING-LIVE.md` Phase 3.
+- **Identity/account rules**: account email is the identity in cloud TCs (server stamps lock
+ identity from the token; client sends only machine name). Sign-out/in as the same account
+ is always safe. Switching accounts with unsent checked-out changes is **blocked** with
+ explicit choices (Send first, or preserve `.bloomSource` + release locally); the server
+ lock stays with the original account. Nothing is ever discarded implicitly.
+- **History tab** (cloud TCs): reads server events (cached locally for offline display);
+ the SQLite-in-book mechanism remains for folder TCs.
+- **Unicode**: NFC normalization for names and paths everywhere.
+
+## Client integration: base-class changes (all folder-backend-behavior-preserving)
+
+1. Backend factory: `TeamCollectionLink` parses `TeamCollectionLink.txt` (folder path or
+ `cloud://sil.bloom/collection/`); factory replaces the three hardcoded
+ `new FolderTeamCollection(...)` sites in `TeamCollectionManager`. Old Bloom versions read
+ the cloud link as a folder path and land in the safe Disconnected state.
+2. Lock seams: `protected virtual TryLockInRepo/UnlockInRepo` (folder keeps read-modify-write;
+ cloud is one conditional RPC).
+3. Status-write discipline: cloud `WriteBookStatusJsonToRepo` diff-dispatches to the narrowest
+ RPC; audit the ~10 `WriteBookStatus` callers.
+4. Capability flags: `SupportsVersionHistory`, `SupportsSharingUi`, `RequiresSignIn` — UI
+ branches on capability, never on concrete type.
+
+`SyncAtStartup`, clobber/conflict logic, message log, local status files, `DisconnectedTeamCollection`
+are reused unchanged. `CloudRepoCache` (thread-safe, persisted snapshot + event cursor) makes
+the synchronous status calls cheap and hydrates Disconnected mode.
+
+## UI changes (summary)
+
+Settings gains the cloud share path + a Sharing panel (approved-accounts list) replacing the
+free-text admin list for cloud TCs; the cloud create dialog is sign-in → confirm immutable
+name → initial Send (no folder chooser, no restart). Collection chooser gains "Get my Team
+Collections". Collection tab: same status chip (now live/precise), status dialog gains
+"Receive Updates" (successor of Reload Collection) and "Send All", a Share button appears, the
+per-book panel carries over nearly unchanged (+ signedOut / updatesAvailable states). All new
+strings via the XLF pipeline with Send/Receive terminology.
+
+## Edge cases
+
+The full ~135-case disposition matrix (every case in src/BloomTests/TeamCollection and the
+TeamCollection.cs decision logic → impossible-now / changed / unchanged / disallowed, plus 15
+new cloud-specific cases) lives in the review artifact, Level 4, and drives the test plan.
+Headline: most Dropbox-era cases exist only because Dropbox is not transactional and become
+structurally impossible; all offline-work-vs-moved-on-repo collisions converge on the unified
+`.bloomSource` + incident-event recovery.
+
+## Test plan (summary)
+
+- C# unit suites (src/BloomTests/TeamCollection/Cloud/): link/factory, lock seams, cache,
+ manifest, transfer, member-by-member backend tests, the ported SyncAtStartup matrix
+ (asserting `.bloomSource` + incident events), monitor, auth, sharing API.
+- Server: pgTAP (RLS matrix, checkout concurrency, claiming requires verified email,
+ last-admin guard, cursor, tombstone/undelete, name uniqueness) + Deno tests per edge function.
+- Component (vitest browser mode): SharingPanel, chooser, status panel states, create dialog.
+- E2E (Playwright over CDP driving real Bloom.exe; local Supabase + MinIO/sandbox):
+ E2E-1 create · E2E-2 two-instance collaboration · E2E-3 checkout contention ·
+ E2E-4 forced check-in recovery (`.bloomSource` + incident) · E2E-5 approved accounts across
+ two machines · E2E-6 kill-mid-Send/resume · E2E-7 un-team adoption ·
+ E2E-8 Receive-during-Send coherence (mandated) · E2E-9 new-book lifecycle/phantom/name-race ·
+ E2E-10 account-switch safety.
+- Standing gate: the entire existing folder-TC suite passes unchanged on every branch.
+
+## Provisioning
+
+Supabase CLI (migrations in `supabase/` in this repo — decided location; local dev/CI via
+`supabase start`) + AWS CLI/Terraform script creating the versioned bucket, lifecycle rules,
+`bloom-teams-broker` role, and the assume-role-only IAM user for edge functions. Docker for
+local Supabase + MinIO. Two hosted Supabase projects (production, sandbox).
+
+## Roadmap
+
+M0 enablers (1–2wk) → M1 create + read-only + join-by-listing + polling + modal Receive
+(4–6wk) → M2 checkout + Send + force-unlock + `.bloomSource` recovery (4–6wk) → M3 sharing
+panel + Get-my-Team-Collections (2–3wk) → M4 realtime + reconnect hardening (2–3wk) →
+M5 adoption polish + server-fed history tab + dogfood (2–3wk). Build is orchestrated per
+[CloudTeamCollections/IMPLEMENTATION.md](CloudTeamCollections/IMPLEMENTATION.md).
+
+## Open items
+
+- Auth option A/B/C: colleague decision pending (brief in the artifact).
+- Safety-window duration: recorded as 7 days ("option one" reading — if "one day" was meant,
+ transaction lifetime shrinks to ~12h; invariant: transaction lifetime < expiry floor).
diff --git a/Design/CloudTeamCollections/CONTRACTS.md b/Design/CloudTeamCollections/CONTRACTS.md
new file mode 100644
index 000000000000..058861ec63a2
--- /dev/null
+++ b/Design/CloudTeamCollections/CONTRACTS.md
@@ -0,0 +1,152 @@
+# Cloud Team Collections — frozen API contracts (v1)
+
+Changes to this file require an orchestrator commit and a version-note bump here.
+**Contract version: 1.5** (11 Jul 2026 — per-collection-copy "seat" on checkouts, additive
+(bug #0, John's ruling): `checkout_book`/`checkout_book_takeover` gain an optional third
+`seat` parameter (client-computed stable hash of the local collection folder path) and
+return `locked_seat`; `get_collection_state`/`get_changes` book rows carry `locked_seat`;
+takeover requires machine AND seat to match, and a NULL stored seat never matches
+(fail-safe). v1.4, 9 Jul: added the `checkout_book_takeover` RPC, additive
+(account-switch behavior, dogfood batch item 9); `checkin_start_tx`/`checkin_finish_tx`
+unchanged. v1.3, 8 Jul: added the "Auth (Option A)" section: the token-receipt endpoint
+BloomLibrary2's `src/editor.ts` forwards Firebase tokens to. v1.2, 7 Jul: added the
+`get_book_manifest` RPC, additive; the Receive path needs a per-book file manifest and no
+existing RPC carried one. v1.1, 6 Jul: two wire-format clarifications under "Postgres
+RPCs"; no semantic changes.)
+
+## Link file
+
+`TeamCollectionLink.txt` content is either a folder path (legacy folder TC) or
+`cloud://sil.bloom/collection/` where `` = the Bloom
+CollectionId GUID (also the server `collections.id`).
+
+## Auth
+
+Bearer JWT on every request (**Option A, decided 8 Jul 2026**: Supabase third-party Firebase
+auth — the JWT is the Firebase ID token itself, unmodified; `CloudAuth` isolates the client
+side of this). Claims used server-side: `sub` (user id), `email`, `email_verified`. Claiming
+an approval requires `email_verified = true`.
+
+## Auth (Option A): token-receipt endpoint
+
+The Bloom-side half of BloomLibrary2's forwarding change (`src/editor.ts`, GOING-LIVE.md
+Phase 3.2, not yet written against this file's *previous* prose — this section is the
+precise text to write it against). Reuses the exact conventions of the pre-existing
+`external/login` endpoint (ExternalApi.cs) that the same BloomLibrary-hosted login page
+already posts back to for the legacy Parse session: same host/port
+(`http://127.0.0.1:{port}/bloom/api/...`, `port` is the query param the login page was opened
+with — see `BloomLibraryAuthentication.LogIn`'s `login-for-editor?port=` URL), same
+CORS/OPTIONS handling, same "POST it and move on" shape. It is a **separate** endpoint, not
+new fields on `external/login`, because the two payloads are independent (a legacy Parse
+sign-in does not imply a Cloud Team Collection one, and vice versa) and the login page may
+call either or both.
+
+**Route**: `POST /bloom/api/external/cloudLogin`
+
+**Request body** (JSON):
+```json
+{ "idToken": "", "refreshToken": "" }
+```
+Both fields are required, non-empty strings. `idToken` is the raw Firebase ID token JWT
+(the login page's own Firebase SDK session already holds this after sign-in); `refreshToken`
+is its paired Firebase refresh token. Bloom derives identity (email/user id/email_verified/
+expiry) **only** from decoding `idToken`'s own claims — it never trusts a separately-supplied
+email or verified flag (see `FirebaseCloudAuthProvider.AcceptExternalSession` /
+`SessionFromIdToken`).
+
+**Reply**: `200` with an empty body on success (`request.PostSucceeded()`, matching
+`external/login`); a non-2xx status with a plain-text error message on failure (e.g. a
+malformed/unparseable token). An `OPTIONS` preflight always succeeds with an empty 200, same
+as every other `external/*` endpoint.
+
+**Side effects on success**: the same as the dev-mode `sharing/login` endpoint —
+`CloudAuth`'s in-memory session is replaced (persisted via `DpapiCloudTokenStore` once the
+production wiring in GOING-LIVE.md Phase 3.5 selects it), the `sharing`/`loginState`
+websocket event fires so any open `useSharingLoginState()` subscriber (e.g. `SignInDialog`)
+re-queries and updates/closes itself, and the Bloom window is brought to the front (matching
+`external/login`'s `Shell.ComeToFront()` — the user's attention is already on the browser tab
+that just finished signing in).
+
+## Postgres RPCs (PostgREST `/rest/v1/rpc/...`)
+
+Wire-format clarifications (v1.1): (1) the implemented SQL functions prefix every parameter
+with `p_`, and PostgREST matches JSON keys to parameter names — so clients send
+`{"p_collection_id": ...}` etc.; the table below keeps the logical (unprefixed) names.
+(2) The `tc` schema is exposed as a separate PostgREST schema: RPC calls must carry the
+`Content-Profile: tc` header (reads: `Accept-Profile: tc`).
+
+| RPC | Args → Result |
+|-----|----------------|
+| `create_collection(id uuid, name text)` | creates collection + caller as sole claimed admin |
+| `my_collections()` | collections where caller's email is approved (claimed or not) |
+| `claim_memberships()` | fills user_id on rows matching caller's verified email |
+| `get_collection_state(collection_id, since_event_id?)` | full/delta snapshot: book rows (locks, current version seq + checksum), collection-file group versions, `max_event_id` |
+| `get_changes(collection_id, since_event_id)` | events + touched book rows (polling/catch-up) |
+| `get_book_manifest(book_id)` | v1.2: per-file current manifest `{bookId, versionId, seq, checksum, files:[{path, sha256, size, s3VersionId}]}` for pinned-version Receive; never-committed books invisible except to their mid-Send lock holder |
+| `checkout_book(book_id, machine text, seat text?)` | conditional lock; returns resulting status (winner's identity on failure). v1.5: also records the caller's `seat` — a stable hash of the local collection folder path identifying WHICH local copy took the lock (never the raw path); returns `locked_seat`. |
+| `checkout_book_takeover(book_id, machine text, seat text?)` | v1.4/v1.5: atomically reassigns another account's lock to the caller ONLY when the existing lock is recorded for the same machine AND the same seat (account-switch, batch item 9 + bug #0: two local copies on one computer are two seats); a NULL stored seat never matches (fail-safe). Returns `{success, locked_by, locked_by_machine, locked_seat, locked_at}` (same shape as checkout_book); emits a CheckOut event only on a genuine handover; safe to call speculatively — no-ops (success:false) when unlocked, already the caller's, locked on a different machine, or locked in a different/unknown seat. Note: `machine` and `seat` are client-asserted, consistent with checkout_book's existing trust model. |
+| `unlock_book(book_id)` | release own lock (undo checkout, no content change) |
+| `force_unlock(book_id)` | admin; audited; emits ForcedUnlock event |
+| `delete_book(book_id)` | requires caller holds the lock; sets `deleted_at`; emits Deleted |
+| `undelete_book(book_id)` | admin; clears tombstone (name-uniqueness enforced) |
+| `rename_check(book_id, new_name)` | advisory uniqueness pre-check |
+| `members: list/add/remove/set_role` | admin-only approved-accounts management; remove force-unlocks that user's checkouts (evented); last-admin guard |
+| `add_palette_colors(collection_id, palette, colors[])` | union merge |
+| `log_event(...)` | client-originated history entries |
+
+All timestamps server-side. All RPCs RLS-gated; books/versions accept no direct writes.
+
+## Edge functions (`/functions/v1/`, JWT-verified; only these hold AWS creds)
+
+### `checkin-start` POST
+Req: `{ collectionId, bookId?, bookInstanceId, proposedName, baseVersionId?, checksum,
+clientVersion, files: [{path, sha256, size}] }`
+- `bookId` null ⇒ first Send of a new book: validates name/instance-id uniqueness; creates the
+ row locked to caller with NO current version (invisible to teammates until first commit).
+- Re-call with the same open transaction ⇒ refreshed credentials, same transactionId.
+200: `{ transactionId, changedPaths[], s3: { bucket, region, prefix,
+credentials: { accessKeyId, secretAccessKey, sessionToken, expiration } } }`
+(creds scoped `tc/{cid}/books/{bookInstanceId}/*`, 1 h)
+Errors: 401/403 · 409 `LockHeldByOther` (+holder) / `BaseVersionSuperseded` / `NameConflict`
+· 426 `ClientOutOfDate`.
+
+### `checkin-finish` POST
+Req: `{ transactionId, comment?, keepCheckedOut? }`
+Verifies each changed object's sha256 attribute; captures s3 version-ids; one DB tx:
+version (metadata) row, current-manifest rows (superseded rows pruned), book row update,
+lock release (unless keepCheckedOut), events (Created+CheckIn for a new book), writes
+`.manifest.json`. 200: `{ versionId, seq }` · 409 `MissingOrBadUploads { paths[] }`
+(re-upload + retry, idempotent) · 410 transaction expired.
+
+### `checkin-abort` POST — `{ transactionId }` → 200.
+
+### `download-start` POST — `{ collectionId }` →
+200 `{ s3: {...} }` read-only creds (`GetObject` + `GetObjectVersion`) scoped `tc/{cid}/*`, 1 h.
+
+### `collection-files-start` / `collection-files-finish` POST
+`{ collectionId, groupKey: 'other'|'allowed-words'|'sample-texts', expectedVersion, files[] }`
+two-phase like check-in; finish bumps the group version atomically; 409 `VersionConflict`
+⇒ client pulls first (repo-wins rule).
+
+## Realtime
+
+Private broadcast channel `collection:{uuid}` (events-table trigger). Message:
+`{ eventId, type, bookId?, versionSeq?, byUserName, byEmail, lock?, name?, groupKey? }`.
+Clients persist `last_seen_event_id`; on (re)connect always run one `get_changes` delta first.
+Event `type` values = existing `BookHistoryEventType` numerics + incident extensions
+(e.g. WorkPreservedLocally).
+
+## S3 layout (bucket versioning ON; lifecycle: abort-multipart 7d, noncurrent expiry ~7d)
+
+```
+tc/{collectionId}/books/{bookInstanceId}/{relativePath} (NFC-normalized)
+tc/{collectionId}/books/{bookInstanceId}/.manifest.json (current manifest backup)
+tc/{collectionId}/collectionFiles/{group}/{relativePath}
+```
+Reads are ALWAYS by (path, s3VersionId) from the committed manifest — never "latest".
+Invariant: check-in transaction lifetime < noncurrent-expiry floor.
+
+## Book-status JSON (client ↔ TeamCollectionApi, additive)
+
+Existing `IBookTeamCollectionStatus` fields unchanged; adds `localVersionSeq?`,
+`repoVersionSeq?`, `signedIn`, backend capability flags.
diff --git a/Design/CloudTeamCollections/GOING-LIVE.md b/Design/CloudTeamCollections/GOING-LIVE.md
new file mode 100644
index 000000000000..fcb338f10b58
--- /dev/null
+++ b/Design/CloudTeamCollections/GOING-LIVE.md
@@ -0,0 +1,232 @@
+# Cloud Team Collections — going-live runbook
+
+How to take Cloud Team Collections from the fully-local dev stack (local Supabase + MinIO +
+dev auth; see `server/dev/README.md`) to real, testable infrastructure, and what must be true
+before the `cloud-collections` branch merges to master. Each step is tagged **[HUMAN]** (needs
+credentials, org access, or a judgment call) or **[AGENT]** (a codeable task an agent can be
+given, with the human reviewing). Steps are ordered; parallelizable groups are noted.
+
+Design context: `../CloudTeamCollections.md` · Contracts: `CONTRACTS.md` · Progress:
+`IMPLEMENTATION.md` (this file expands its "Deferred until real infrastructure" list).
+
+---
+
+## Phase 1 — Decisions (block everything else in Phase 2+)
+
+### 1.1 [DECIDED 8 Jul 2026] Auth option: **A**
+**Option A: Supabase third-party Firebase auth** — Supabase is configured to trust
+Firebase-issued JWTs directly, so the Bloom user signs in with the same BloomLibrary
+(Firebase) account they already have, and that token IS the Supabase credential. Phase 3
+below is therefore actionable; the Bloom-side provider work (3.4) started the same day.
+- **A (chosen)**: BloomLibrary2's login page forwards the Firebase ID + refresh tokens it
+ already holds to Bloom (~5 lines in BloomLibrary2 `src/editor.ts`), and Supabase is set to
+ accept Firebase as a third-party auth provider. Requires one NEW small Firebase Admin cloud
+ function that adds the static `role: "authenticated"` custom claim to every user (plus a
+ one-time backfill over existing users — no custom-claims infrastructure exists today).
+- ~~B~~: exchange the legacy Parse session token — rejected; welds us to bloom-parse-server,
+ which is being decommissioned.
+- ~~C~~: hand-validate Firebase JWTs ourselves per the stale `bloom-parse-server/supabase/`
+ docs — rejected; more code we own, no benefit over A.
+
+### 1.2 [DECIDED 9 Jul 2026] Safety-window duration: **7 days**
+John confirmed the `provision-aws.ps1` default of 7 days for noncurrent-version expiry
+("keeping noncurrent objects for 7 days seems plenty"). Constraint honored: strictly greater
+than the 48-hour checkin-transaction lifetime (`tc.checkin_transactions.expires_at`). No
+script change needed — step 2.1 runs as written.
+
+---
+
+## Phase 2 — Infrastructure provisioning
+
+### 2.1 [HUMAN] Provision AWS (S3 + IAM)
+Run the reviewed-but-never-run script (needs an AWS account + CLI credentials with S3/IAM
+admin rights — that's why it's a human step):
+
+```powershell
+# dry run first:
+server\provision-aws.ps1 -WhatIf
+server\provision-aws.ps1 # defaults: bloom-teams-production + bloom-teams-sandbox, us-east-1
+```
+
+It idempotently creates, per environment:
+- S3 bucket `bloom-teams-` — versioning ON, public access blocked, lifecycle rules
+ (abort incomplete multipart 7d; expire noncurrent versions under `tc/` per step 1.2).
+- IAM role `bloom-teams-broker` — the role the edge functions AssumeRole into to mint
+ short-lived, per-request, per-book-prefix-scoped credentials for clients.
+- IAM user `bloom-teams-broker-caller` — assume-only (its sole permission is sts:AssumeRole on
+ that role). Its access key becomes the edge functions' `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`.
+- IAM user `bloom-teams-admin` — direct S3 permissions, used ONLY server-side (checksum/
+ version-id verification, manifest backup). Key becomes `BLOOM_S3_ADMIN_ACCESS_KEY`/`_SECRET_KEY`.
+
+Before running: review the embedded IAM policy JSON against current least-privilege guidance,
+and note the script's own NOTES section (developed against AWS CLI v2, never executed).
+**Record the two access-key pairs somewhere safe; they are needed in 2.3.**
+
+### 2.2 [HUMAN] Create the hosted Supabase projects
+Two projects (production + sandbox) in the org's Supabase account. For each:
+1. `supabase link --project-ref ` from the repo root.
+2. `supabase db push` — applies the exact same checked-in migrations
+ (`supabase/migrations/*.sql`) the local stack runs. No schema differences exist.
+3. `supabase functions deploy` — deploys the same checked-in edge functions
+ (`supabase/functions/*`).
+4. Record each project's URL and anon key (Dashboard → Settings → API) for 2.4 and Phase 3.
+
+This is how "Supabase gets access to the S3 buckets": the edge functions run INSIDE Supabase
+and read the AWS credentials from Supabase **secrets** (next step). Nothing else links them.
+
+### 2.3 [HUMAN] Set the edge-function secrets (this is the Supabase↔S3 handshake)
+For each project:
+
+```bash
+supabase secrets set BLOOM_CLOUD_LOCAL_MODE=false
+supabase secrets set AWS_ACCESS_KEY_ID=
+supabase secrets set AWS_SECRET_ACCESS_KEY=
+supabase secrets set BLOOM_S3_ADMIN_ACCESS_KEY=
+supabase secrets set BLOOM_S3_ADMIN_SECRET_KEY=
+supabase secrets set BLOOM_S3_BUCKET=bloom-teams-
+supabase secrets set BLOOM_S3_REGION=us-east-1
+# do NOT set BLOOM_S3_ENDPOINT in production — its absence selects real AWS endpoints
+```
+
+`BLOOM_CLOUD_LOCAL_MODE=false` flips `_shared/env.ts`/`s3.ts` from MinIO-AssumeRole local
+credentials to real AWS STS (false is also the default when unset — hosted deployments,
+including any future "dev"-named project, never set it). The names mirror the local
+`server/dev/functions.env` (which is the committed, local-only-constants version of this
+same set).
+
+### 2.4 [AGENT] Security hardening: lock down the `tc.*_tx` RPCs ← REQUIRED before production
+Currently the internal transaction RPCs are EXECUTE-granted to `authenticated` because edge
+functions forward the caller's JWT. A member could call e.g. `checkin_finish_tx` directly and
+bypass the edge function's S3 checksum verification (blast radius limited to their own
+collection, but still). Task: new migration that (a) switches the edge functions to use the
+service-role key with an explicit verified-user-id parameter, and (b) REVOKEs `authenticated`
+from all `*_tx` functions. Convention: never edit merged migrations — new migration files only.
+Verify with a pgTAP test that `authenticated` can no longer execute them. Human review of the
+migration before deploy.
+
+---
+
+## Phase 3 — Auth implementation (after 1.1; assumes Option A)
+
+### 3.1 [HUMAN] Enable Firebase as third-party auth on both Supabase projects
+Dashboard → Authentication → Third-party Auth → add Firebase, pointing at the BloomLibrary
+Firebase project. (This is configuration, not code, and needs org access to both consoles.)
+
+### 3.2 [AGENT, other repo] BloomLibrary2 token forwarding
+The ~5-line change in BloomLibrary2 `src/editor.ts`: after login, forward the Firebase ID +
+refresh tokens to Bloom (the login flow Bloom already hosts in a browser for registration).
+Lives in the BloomLibrary2 repo; needs that repo's normal review/deploy cycle.
+
+### 3.3 [AGENT, Firebase] `role: "authenticated"` custom-claim function + backfill
+A small Firebase Admin cloud function that stamps the static claim on user creation, plus a
+one-time backfill script over existing users. Supabase requires this claim on third-party JWTs.
+[HUMAN]: deploy it (Firebase console/CLI credentials) and run the backfill.
+
+### 3.4 [AGENT] Real `CloudAuth` provider in Bloom
+Implement the production provider behind the existing `CloudAuth`/`CloudAuthProvider` seam
+(`src/BloomExe/TeamCollection/Cloud/`): accept the forwarded Firebase tokens, refresh as
+needed, expose the same `GetLoginState`/`SignIn`/`SignOut` surface the dev provider has.
+Includes the deferred **persistent token store** (survive Bloom restarts; the dev provider
+skips this). `CloudAuthMode.Cloud` already exists as the selector. The server-side
+`tc.jwt_email_verified()` helper already isolates the Firebase-vs-GoTrue `email_verified`
+claim-shape difference — verify it against a real Firebase JWT and adjust in a new migration
+if needed. Claiming an approval requires `email_verified` (all BloomLibrary accounts qualify).
+
+### 3.5 [AGENT] Client production defaults
+`CloudEnvironment.cs` compiled defaults currently point at the local stack. Change to: real
+production Supabase URL + anon key, empty `DefaultS3Endpoint` (its absence selects real AWS
+virtual-hosted style — `S3ForcePathStyle` already keys off this), production bucket,
+`CloudAuthMode.Cloud`. Sandbox/dev keep working via the `BLOOM_CLOUDTC_*` env-var overrides
+(document a "sandbox profile" env block in server/dev/README.md). Anon keys are public by
+design; committing them is fine.
+
+---
+
+## Phase 4 — Verification against real infrastructure (sandbox)
+
+### 4.1 [AGENT] Parity re-verification
+Re-run `server/dev/parity-check` against the sandbox bucket + real STS: sha256 checksum
+headers, S3 version-id capture on PUT, lifecycle behavior, AssumeRole session-policy scoping.
+These were verified against MinIO on the explicit assumption they'd be re-checked on AWS.
+[HUMAN]: supply temporary credentials for the run.
+
+### 4.2 [AGENT] E2E matrix against sandbox
+Point the E2E harness at sandbox via env vars (the harness's `devStack.ts` values become a
+config block) and run the full matrix. Expect to keep per-scenario reset working — that needs
+a sandbox-reset path (`supabase db reset --linked` is destructive and slow; an agent should
+add a `tc`-schema truncate + bucket-prefix-clear script instead). Also re-run the two
+`[Explicit]` C# live tests (`CloudTeamCollectionLiveTests`) with sandbox env vars.
+
+### 4.3 [DECIDED 9 Jul 2026] AWSSDK.S3 version bump: **on this branch, before go-live**
+John: "It's fine for this branch to bump the S3 sdk version." [AGENT] executes the bump and
+runs the full suites (cloud filter + FULL BloomTests once + the E2E matrix — AWSSDK is also
+used by the BloomLibrary web-upload code, so the blast radius is wider than cloud TCs).
+**[HUMAN] added to the test plan at John's request: after the bump, manually verify that web
+book UPLOAD (publish to bloomlibrary.org) and DOWNLOAD (get a book from bloomlibrary.org
+into Bloom) are unaffected.** Queued as item 10 in `orchestration/DOGFOOD-BATCH-1.md`.
+EXECUTED 9 Jul 2026 (merged): AWSSDK.S3/Core 3.5.x → 4.0.100.3; MinIO-facing clients pin
+checksum behavior to WHEN_REQUIRED (v4's WHEN_SUPPORTED default breaks S3-compatibles);
+real-AWS BloomS3Client keeps v4 defaults; full BloomTests baseline-identical (3036/0).
+**[HUMAN/cross-repo] BloomHarvester extends BloomS3Client in its own repo — it must take a
+matching AWSSDK v4 bump before consuming a Bloom release containing this change.**
+Also worth a quick [HUMAN] smoke when convenient: problem-report book upload (YouTrack /
+bloom-problem-books bucket) rides the same SDK.
+
+---
+
+## Phase 5 — Product gaps to close before REAL-USER testing
+
+Found during Wave-4 E2E work (see `tasks/09-e2e.md` progress log for full detail). The
+experimental-feature flag gates all of this UI, so merging to master does NOT require these —
+but giving the feature to real testers does:
+
+- **[DECIDED 9 Jul 2026 → AGENT] Account-switch behavior (E2E-10).** John's full spec is
+ recorded as item 9 in `orchestration/DOGFOOD-BATCH-1.md`: local access is unrestricted and
+ only shared-data operations are gated by the CURRENT logon; opening a collection joined
+ under another account REFUSES (with admin emails + last local editor named) when the new
+ logon is not a member, and opens CONNECTED (with atomic checkout takeover on first edit)
+ when it is. Supersedes the earlier "block logout with unsent changes" sketch. E2E-10
+ becomes the acceptance test once implemented.
+- **[DECIDED + IMPLEMENTED 9 Jul 2026] Cloud recovery (E2E-4's blocked half).** John's
+ decision (batch item 8): a sync that must overwrite locally-changed content goes ahead but
+ first preserves the previous local version as a `.bloomSource` in Lost and Found (+ server
+ incident). Implemented as a pure checksum guard in the auto-apply worker and the Sync
+ loop (commit e0526fa30); deliberately NOT the persist-checkout-state-to-local-file
+ alternative, which was only needed to reproduce folder-TC blocking semantics. Remaining
+ [AGENT] follow-up: extend E2E-4's spec to cover the now-reachable preserve path.
+- **[POLICY DECIDED 9 Jul 2026 → AGENT] Subscription-tier check timing.** John: cloud TCs
+ require the SAME subscription tier as folder Team Collections — no new policy, reuse the
+ existing FeatureName.TeamCollection gate. Remaining [AGENT] work is purely the timing bug:
+ `CheckDisablingTeamCollections` can intermittently disconnect a cloud TC when the
+ subscription check races cloud sign-in (harness works around it with a test subscription
+ code); make the check deterministic for cloud TCs.
+- **[DONE 9 Jul 2026] Preview pane refresh on Receive** — fixed by batch item 4+5's
+ auto-apply (the worker refreshes the preview when the applied book is selected). Still
+ open, nice-to-have: join-conflict states show generic errors (dedicated resolution dialog
+ was deferred from task 07).
+
+---
+
+## Phase 6 — Merging `cloud-collections` to master
+
+Prerequisites (mostly already true; verify at merge time):
+
+1. [AGENT] Final rebase onto master; full folder-TC regression suite green (the widened
+ filter `~Cloud|~TeamCollection|~SharingApi` plus the FULL BloomTests run once); vitest
+ suite green; E2E matrix green locally.
+2. [HUMAN] Confirm the experimental flag ("Cloud Team Collections (experimental)" in
+ Settings → Advanced) is the ONLY way the new UI appears — merged code must be inert for
+ everyone else. (Verified in Wave 4; re-verify after rebase.)
+3. [AGENT] XLF check: all new strings `translate="no"`, en-only, and **no `--` inside any
+ ``** (crashes every launch; rule + history in `.github/skills/xlf-strings/SKILL.md`).
+4. [HUMAN] Normal PR review + team heads-up that `server/`, `supabase/`, and
+ `src/BloomTests/e2e/` are new top-level areas.
+5. [DECIDED 9 Jul 2026] Dogfood plan: NO existing team collections are touched. Create
+ fresh test collections, turn them into cloud TCs, have various testers join and try
+ things out (against the sandbox infra from Phases 2–4). (Task 10's
+ `docs/user-walkthrough.md` is the tester-facing doc; feedback channel: whatever is
+ convenient — nothing formal was mandated.)
+
+Merging BEFORE Phases 2–5 are done is fine and useful (code is flag-gated and local-stack
+self-sufficient); real-user testing needs Phases 2–4 plus at least the account-switch and
+recovery items from Phase 5 triaged.
diff --git a/Design/CloudTeamCollections/IMPLEMENTATION.md b/Design/CloudTeamCollections/IMPLEMENTATION.md
new file mode 100644
index 000000000000..faea071413e6
--- /dev/null
+++ b/Design/CloudTeamCollections/IMPLEMENTATION.md
@@ -0,0 +1,305 @@
+# Cloud Team Collections — implementation master checklist
+
+Design: [../CloudTeamCollections.md](../CloudTeamCollections.md) · Contracts: [CONTRACTS.md](CONTRACTS.md)
+Rules: agents tick checkboxes **only in their own task file**; this master file is updated
+**only by the orchestrator**. Every task PR must build, pass its acceptance tests, and pass
+the entire existing folder-TC test suite.
+
+## Local-first development strategy
+
+All development and testing through Wave 4 runs against a **fully local stack**: no real S3
+bucket, no hosted Supabase project, and no Firebase/BloomLibrary auth changes are needed to
+start. Setup and details live in [tasks/11-local-dev-stack.md](tasks/11-local-dev-stack.md).
+
+- **Database + API**: local Supabase (`supabase start`, Docker) — the identical Postgres
+ schema, RLS, RPCs, edge functions (`supabase functions serve`), and realtime that production
+ will use. Migrations written now ARE the production migrations. (SQLite rejected: it has no
+ PostgREST/RLS/edge-function equivalents, so we would build a parallel backend and then throw
+ it away; local Supabase is the product stack itself.)
+- **S3 substitute**: MinIO in Docker — an S3-compatible server that stores objects on the
+ local file system, with object versioning and checksum support. `BloomS3Client` /
+ TransferUtility talk to it through a service-URL override; nothing else in the client
+ changes. In dev mode the edge functions return static MinIO credentials **in the same JSON
+ shape as the production STS response**, so CONTRACTS.md is unchanged (per-book credential
+ scoping is a production security measure, not a functional dependency).
+- **Auth substitute**: local GoTrue (bundled with local Supabase) email/password with
+ auto-confirm — any email + password signs up/in as a valid, verified user ("a backend that
+ accepts any login"), yielding real JWTs so RLS and every RPC run unchanged. Real
+ BloomLibrary/Firebase sign-in (Option A/B/C) plugs in later behind the existing `CloudAuth`
+ seam; the server side isolates the token-shape difference (Firebase `email_verified` claim
+ vs GoTrue confirmation) in one SQL helper, `tc.jwt_email_verified()`.
+- **Two instances on one machine**: each Bloom instance gets its own collection folder plus
+ `BLOOM_CLOUDTC_USER` / `BLOOM_CLOUDTC_PASSWORD` env-var overrides so it runs as a distinct
+ dev identity (bypassing the shared stored-token settings). This is the Wave 3 manual smoke
+ and the mechanism the Wave 4 E2E harness scales up.
+- **Environment switching**: every external endpoint (Supabase URL, anon key, S3
+ endpoint/bucket/path-style, auth mode) resolves through one `CloudEnvironment` config
+ (env vars over compiled defaults; owned by task 03). Cutover to the real bucket, hosted
+ Supabase, and real sign-in is configuration plus the deferred-infrastructure list below —
+ zero protocol or schema change.
+
+## Branching
+
+- Integration branch: `cloud-collections` (base branch: **confirm with John** — master vs the
+ active Version6.x branch). Base merged into integration weekly.
+- One branch + one git worktree per task; PRs into the integration branch, merged one at a
+ time by the orchestrator after code review.
+
+## Waves
+
+| Wave | Tasks | Parallel? | Gate |
+|------|-------|-----------|------|
+| 0 | [00-enablers](tasks/00-enablers.md) | No — orchestrator-led (shared hot files) | Existing TC suite green, zero behavior change |
+| 1 | [11-local-dev-stack](tasks/11-local-dev-stack.md) · [01-server-schema](tasks/01-server-schema.md) · [02-edge-functions](tasks/02-edge-functions.md) · [03-auth](tasks/03-auth.md) · [07-ui-setup](tasks/07-ui-setup.md) | Yes — zero file overlap (contracts frozen first) | Each task's acceptance tests; 11's stack-smoke script green |
+| 2 | [04-client-core](tasks/04-client-core.md) · [08-ui-collection-tab](tasks/08-ui-collection-tab.md) | Yes | Unit suites green |
+| 3 | [05-cloud-backend](tasks/05-cloud-backend.md) → [06-api-endpoints](tasks/06-api-endpoints.md) → UI wiring | **Sequenced** (shared files) | Two-instance smoke on ONE machine against the local stack |
+| 4 | [09-e2e](tasks/09-e2e.md) · [10-adoption](tasks/10-adoption.md) | Yes | Full E2E matrix green against the local stack; dogfood |
+
+## Shared-file schedule (no two concurrent tasks may touch the same one)
+
+| File | Owner |
+|------|-------|
+| TeamCollection.cs, TeamCollectionManager.cs | Wave 0 only (orchestrator) |
+| TeamCollectionApi.cs | 06 only |
+| CollectionChooserDialog | 07 only |
+| FeatureRegistry.cs, BloomExe.csproj | Orchestrator at merge time |
+| supabase/** | 01/02 (01 owns migrations; 02 owns functions/); 11 owns config.toml auth/dev settings + seed |
+| server/dev/** (docker-compose, seeds, smoke script, docs) | 11 only |
+| Cloud/CloudEnvironment.cs | 03 only |
+
+## Deferred until real infrastructure is available (tracked, NOT blocking)
+
+**The detailed go-live runbook — ordered steps, each tagged [HUMAN] or [AGENT], covering AWS
+provisioning, hosted Supabase, the Supabase↔S3 secret handshake, Firebase auth (Option A),
+sandbox verification, remaining product gaps, and the merge-to-master checklist — is
+[GOING-LIVE.md](GOING-LIVE.md).** The list below remains as the summary index.
+
+Each of these is a config/provisioning swap, not a code change, thanks to the seams above.
+
+- [ ] Auth Option A/B/C decision (colleague review) and, for Option A: the BloomLibrary2
+ `src/editor.ts` token-forwarding change + the Firebase Admin claim function (other repos).
+ Then: implement the real `CloudAuth` provider behind the existing interface.
+- [ ] Run `server/provision-aws` (script is written and reviewed in task 02) against real AWS:
+ buckets, versioning, lifecycle, `bloom-teams-broker` role, assume-only IAM user.
+- [ ] Create hosted Supabase projects (production + sandbox); `supabase db push` the same
+ migrations; deploy the same edge functions; `supabase secrets set` the AWS credentials.
+- [ ] Flip edge functions from static-MinIO-credential dev mode to real STS (env switch).
+- [ ] Re-verify MinIO/AWS parity assumptions against the real bucket (sha256 checksum headers,
+ s3 version-id capture, lifecycle behavior) and re-run the E2E matrix against sandbox.
+- [ ] **Security hardening (REQUIRED before production):** the internal `tc.*_tx` RPCs are
+ currently EXECUTE-granted to `authenticated` because edge functions forward the
+ caller's JWT (identity stamping via auth.jwt()). A member could therefore call e.g.
+ `checkin_finish_tx` directly, bypassing the edge function's S3 checksum verification
+ (blast radius: corrupting manifests in their own collection only — membership/lock
+ still enforced). Before cutover: switch edge functions to the service-role key +
+ explicit verified-user-id parameter, then revoke `authenticated` from all `*_tx`
+ functions.
+
+## Status
+
+- [x] Wave 0 complete (folder backend provably unchanged — 208/208 TC tests green)
+- [x] Wave 1 complete 7 Jul 2026 (local dev stack live; schema+RPCs pgTAP-green; edge
+ functions live-verified; auth skeleton w/ dev provider; UI shells tested)
+ - [x] 01-server-schema DONE — pgTAP 42/42 green on the live local stack (6 Jul 2026)
+ - [x] 11-local-dev-stack DONE — full stack verified on **Podman 5.8.3** (rootful, WSL2,
+ Docker-compat pipe) instead of Docker Desktop: MinIO up w/ versioning+lifecycle,
+ dev sign-in works, parity spike 4/4, smoke green. README documents the recipe.
+ - [x] 02-edge-functions DONE — six functions live-verified (26/26 integration checks on
+ the local stack incl. MinIO AssumeRole dev creds); Deno 32/32; provision-aws
+ authored-not-run. Found+fixed: get_collection_state new-book leak (01's file);
+ edge_runtime per_worker; MinIO joined to the supabase network (gvproxy hang).
+ - [x] 03-auth DONE — CloudEnvironment/CloudAuth(dev provider)/CloudCollectionClient;
+ 46/46 cloud + 244/244 folder-TC tests; live-verified vs local stack. Deferred:
+ persistent token store; real provider awaits Option A/B/C; human GUI smoke + 2h soak.
+ - [x] 07-ui-setup (shells) DONE — SharingPanel, cloud create dialog, chooser
+ "Get my Team Collections", registration email lock; 29/29 component tests;
+ XLF en-only. Wiring to real endpoints deferred to Wave 3 (after 06) as planned.
+- [x] Wave 2 complete 7 Jul 2026 — 04 client core (83/83 cloud, 281/281 folder-TC);
+ 08 collection-tab shells (60/60 component tests; folder path zero-extra-requests)
+- [x] Wave 3 complete 7 Jul 2026 — GATE PASSED: two-instance manual smoke on one machine
+ (checkout contention visible immediately; Send/Receive round trip with real content;
+ automatic status propagation via polling). Twelve real bugs found+fixed+pinned during
+ the smoke; see merge log.
+- [ ] Wave 4 complete
+ - [x] 09 harness + E2E-1/E2E-2 DONE 8 Jul 2026 — Playwright-over-CDP harness at
+ `src/BloomTests/e2e/` (build-once/launch-many, per-scenario DB+MinIO+scratch reset,
+ DB/S3 verification, experimental-flag automation); E2E-1 (1.4 min) and E2E-2
+ (2.5 min, the automated two-instance smoke) green on orchestrator re-runs.
+ - [x] 09 scenarios E2E-3..9 DONE 8 Jul 2026 — all green individually AND each has passed
+ in matrix context; found+fixed 3 product bugs (UpdateUiForBook NRE = the Wave-3
+ latent recovery NRE; MapError error-envelope mismatch; LogEvent p_message). E2E-4
+ partial (`.bloomSource` recovery unreachable via cloud — product decision needed);
+ E2E-10 blocked (account-switch safety unimplemented — product decision needed).
+ See tasks/09-e2e.md findings 1–10.
+ - [x] Acceptance PASSED 9 Jul 2026: **full matrix 13/13 green in one run** (29.7 min,
+ idle machine) — all ten planned scenarios plus the join-auto-open pin. What made it
+ converge after five 8–11/12 runs: the finding-9 product fix (automation mode logs
+ errors instead of hanging on modal problem-report/notify/progress dialogs — approved
+ by John, gated strictly on --automation) plus harness hardening (kill-by-PID,
+ propagation polls budgeted past the organic 60s cycle, status-probe fallback).
+ - [x] 10-adoption DONE 8 Jul 2026 — all 7 polish items; see merge log.
+ - [ ] Dogfood (needs GOING-LIVE.md phases 2–4 for real-infra, or a local-stack pilot).
+- [ ] Real-infrastructure cutover complete (deferred list above)
+- [x] Auth option DECIDED 8 Jul 2026: **Option A** (Supabase third-party Firebase auth).
+ Bloom-side provider work unblocked (see GOING-LIVE.md Phase 3 and task 12); the
+ BloomLibrary2 token-forwarding change and the Firebase custom-claim function/backfill
+ remain other-repo/[HUMAN] items.
+- [ ] Safety-window duration confirmed (7 days vs 1 day)
+
+## Merge log
+
+(orchestrator appends: date · task · PR · notes)
+
+- 9 Jul 2026 · dogfood bug: checkin comment lost · direct commit · The user's
+ "what did you change?" message was written only to the book's local history.db (correct
+ and sufficient for folder TCs, where history.db rides inside the .bloom file), but cloud
+ history is displayed from the server's event log — and PutBookInRepo never forwarded the
+ comment to checkin-finish, so it silently vanished for everyone. Fix: checkinComment
+ threaded through PutBook/PutBookInRepo into CheckinFinish (server p_comment was already
+ fully plumbed into tc.versions.comment and the event message). Live round-trip test now
+ asserts the comment appears in get_changes. Found by John while dogfooding.
+- 8 Jul 2026 · 12-real-auth (Option A seams) · merged locally · Same-day implementation of
+ everything the Option A decision unblocked in this repo: FirebaseCloudAuthProvider
+ (identity strictly from ID-token claims; securetoken refresh; signature verification
+ deliberately delegated to Supabase per the provider's doc comment), DPAPI-persistent
+ token store, `POST external/cloudLogin` receipt endpoint (CONTRACTS.md v1.3 documents the
+ exact shape the BloomLibrary2 editor.ts change must target — note it should POST to this
+ NEW endpoint, not add fields to external/login), tc.jwt_email_verified() verified
+ Firebase-ready (no migration needed), reference Firebase Admin claim function + backfill
+ under server/firebase/. Agent found+fixed a real cross-end mismatch: CloudAuthMode was
+ "real" in C# but "cloud" in the TS contract; also populated the emailVerified field TS
+ had declared but C# never sent. Verified independently: C# widened filter 359/359;
+ pgTAP 42/42; vitest 5/5. Still deferred (GOING-LIVE 3.1–3.3): hosted-Supabase Firebase
+ config, the BloomLibrary2 change itself, Firebase deploy + backfill run.
+
+- 8 Jul 2026 · post-merge E2E gate · direct commits · The gate caught ONE real merge bug:
+ task 10's new XLF `` texts contained double hyphens, which L10NSharp parses as
+ illegal XML-comment content — EVERY Bloom launch crashed at startup in SetUpLocalization
+ (no unit/component test can catch this; only a real launch reads the installed XLF).
+ Fixed + rule pinned in .github/skills/xlf-strings/SKILL.md. Remaining gate failures were
+ environmental, proven by a pre-merge control run failing identically: (a) a locked Windows
+ session stalls WebView2 at about:blank indefinitely (E2E needs an unlocked desktop);
+ (b) createCloudTeamCollection deadlocks if posted while the workspace WebView2 is still
+ initializing (UI-thread handler + modal progress dialog vs nested message pump — E2E-1
+ now uses E2E-2's connect-before-trigger pattern as a guard). Full diagnosis in task 09's
+ progress log, findings 7–8.
+
+- 8 Jul 2026 · 09-e2e (harness + E2E-1/2) · merged locally · Harness encodes every smoke-test
+ environment rule (Release build MANDATORY — Debug shows a blocking attach-debugger dialog on
+ any positional arg; build-once/launch-many; foreign-Bloom fail-loud; per-scenario
+ `supabase db reset` + `mc` bucket clear + scratch wipe; user.config flag automation).
+ E2E-1 and E2E-2 green on orchestrator re-runs after the agent's runs were starved by
+ concurrent sessions (~6GB free RAM — diagnosis confirmed by clean re-run). Two product
+ findings REPORTED for follow-up, not fixed: (1) every ReactDialog-hosted WebView2 requests
+ the same fixed remote-debugging port, so secondary dialogs are never CDP-reachable —
+ harness drives their backend endpoints directly instead; (2) checkout/check-in buttons
+ ignore CDP-synthesized clicks (root cause undiagnosed; direct API used). Also found:
+ WebView2 temp-profile folders leak per launch (harness cleans them in globalSetup);
+ ~1s endpoint-registration race after BLOOM_AUTOMATION_READY (harness retries 404s).
+ E2E-3..10 remain; E2E-4 must reproduce the recovery-path NRE.
+
+- 8 Jul 2026 · 10-adoption (+ Wave-3 polish list) · merged locally · All 7 items: proper
+ "Cloud Team Collections (experimental)" checkbox in Settings→Advanced (ends the
+ user.config hack); pull-down auto-opens the joined collection; un-team cleanup
+ (CleanStaleTeamCollectionArtifacts) + TeamCollectionLinkConflictException guard with
+ fix-instructions message; first-Receive reconcile verified-by-reading (no checksum
+ reconcile happens — matches folder-TC behavior, documented as known limitation);
+ user walkthrough doc (Design/CloudTeamCollections/docs/user-walkthrough.md); XLF sweep
+ (11 en entries; fixed a TeamCollection.ConflictingCollection id collision); analytics
+ audit (cloud join + Receive Updates events added, Backend=Cloud verified elsewhere).
+ Agent also found+fixed: Team Collection settings tab was invisible when ONLY the cloud
+ flag was on. Orchestrator review fixes: pullDown replied with the collection FOLDER but
+ workspace/openCollection needs the .bloomCollection FILE path (renamed field to
+ collectionPath); doc-comment placement. Verified: C# widened filter 332/332; vitest
+ 29/29 on touched files.
+
+- 7 Jul 2026 · two-instance smoke (Wave-3 gate) · direct commits · PASSED after fixing 12
+ live-found bugs: members_add scalar-response crash; missing claim_memberships in join;
+ identity-model registration-vs-account comparisons (4 sites incl. OkToCheckIn, whose
+ false conflict ALSO exposed the unified-recovery clobber + .bloomSource save working);
+ 'null null' display; BookButton anonymous avatar; missing experimental-feature checkbox
+ (flag set via user.config for now — proper Advanced-settings checkbox still owed);
+ collection-file mirror-delete stripping TeamCollectionLink.txt; update-Send committing
+ changed-files-only manifests (data-loss class — server was right, client wrong; Receive
+ now also refuses empty manifests); cloud change events discarded by the .bloom-suffix
+ contract; Receive Updates now refreshes UI immediately. Deferred niceties logged:
+ selected-book PREVIEW pane doesn't refresh on Receive until reselect (old base-code
+ ENHANCE); pull-down doesn't auto-open; recovery-path NRE reproduction (E2E-4).
+
+- 7 Jul 2026 · ui-wiring · merged locally · Dispatcher fixes a live folder-TC create-dialog
+ breakage (WireUpForWinforms last-caller-wins; three instances of the bug class fixed,
+ regression-tested). Sign-in dialog; SharingPanel + pull-down wiring done. Orchestrator
+ fix on cloud-collections: SharingApi claimed-detection treated JSON-null user_id as
+ claimed (JTokenType.Null gotcha) — found because SharingApiTests match NO task's filter;
+ the widened mandatory filter is now recorded in orchestration/RESUME.md. Post-rebase
+ full suite 318/318. Known smoke-test limitations: pull-down doesn't auto-open the new
+ collection; join-conflict states show generic errors (matching-flags endpoint TBD);
+ real-auth mode intentionally still a placeholder.
+
+- 7 Jul 2026 · 05-cloud-backend · merged locally · Live Send→Receive→lock round trip green.
+ Agent's live test found+fixed 2 integration bugs (RestSharp serializer mangling JTokens;
+ S3 keys built at wrong prefix level). Orchestrator base fixes at review: AttemptLock now
+ honors TryLockInRepo refusal; BookHistoryEventType.WorkPreservedLocally=100. Findings
+ routed forward: (a) server stamps locked_by/created_by with auth UUID — 06 adds a
+ migration surfacing lockedByEmail/Name for display; (b) collection-file groups have no
+ pinned per-file manifest RPC (client reads latest from S3) — acceptable dev-mode gap,
+ revisit before production; (c) checkin-start/finish omit the new book's server id —
+ client refreshes state post-commit (works; consider contract addition later);
+ (d) SyncAtStartup matrix only partially ported — remainder folded into task 09's scope.
+
+- 7 Jul 2026 · 08-ui-collection-tab · merged locally · Survived one session-limit
+ interruption (WIP preserved). Orchestrator review fix: the capability/experimental-flag
+ hooks fetched per component mount — BookButton would have issued hundreds of identical
+ requests per Collection-tab visit; now cached once per page load with test-reset seams
+ in vitest.setup. 60/60 re-verified. Wiring of the ~9 mocked endpoints lands with task 06.
+ WAVE 2 COMPLETE.
+
+- 7 Jul 2026 · 04-client-core · merged locally · 83/83 cloud (re-verified) + 281/281
+ folder-TC. Three findings for later tasks: (1) CONTRACT GAP — no RPC returns a book's
+ per-file manifest for Receive; decide at 05 launch (likely additive get_book_manifest
+ RPC, CONTRACTS bump) vs reading S3 .manifest.json. (2) AWSSDK.S3 3.5.3.10 predates
+ native checksum properties; manual x-amz-checksum-sha256 header live-verified — SDK bump
+ is a deliberate SEPARATE follow-up (publish path shares the package). (3) Task 05 must
+ point CloudBookTransfer.DownloadFiles at a temp book folder and do the final whole-
+ directory swap itself (the class's per-file move loop after full verification is not a
+ single atomic dir swap).
+
+- 7 Jul 2026 · 07-ui-setup · merged locally · Orchestrator review fix: the chooser's cloud
+ section rendered UNGATED for all users — now behind the experimental feature (re-verified
+ 29/29 after fix). Registration/settings changes are additive and folder-safe. Deferred to
+ Wave 3 wiring (documented in task file): SharingPanel into settings' isTeamCollection
+ branch; JoinCloudCollectionDialog's matching logic into the chooser's onPullDown.
+ WAVE 1 COMPLETE.
+
+- 7 Jul 2026 · 02-edge-functions · merged locally · Two agent interruptions survived via
+ per-step commits (the gvproxy hang the agent later diagnosed was likely the stall cause).
+ Independently re-verified Deno 32/32 + pgTAP 42/42. In-place edit of applied migration
+ 20260706000003 accepted THIS TIME (nothing deployed anywhere yet); convention from now
+ on: schema changes to merged migrations arrive as NEW migration files. Deferred-infra
+ list gains the *_tx grant hardening item.
+
+- 6 Jul 2026 · 03-auth · merged locally · Reviewed all three classes; one cosmetic fix
+ (stranded doc comment). Evidence: 46/46 cloud, 244/244 folder-TC, live GoTrue sign-in +
+ RPC error-shape verification, [Explicit] two-concurrent-sessions test green. Note for 04:
+ build RPC/edge wrappers on CallRpc/CallEdgeFunction; RPC errors carry Postgres codes
+ (typed CONTRACTS codes are edge-function-shaped). Dev default AnonKey is empty — devs set
+ BLOOM_CLOUDTC_ANON_KEY from `supabase status` (consider compiling in the stable local
+ demo key later).
+
+- 6 Jul 2026 · 00-enablers · merged locally · Reviewed diff line-by-line; seams preserve
+ folder behavior; 208/208 TeamCollection tests (24 new) verified against fresh binaries.
+ Note: TryLockInRepo gained a BookStatus param vs the task file (avoids redundant GetStatus).
+- 6 Jul 2026 · 01-server-schema · merged locally · Orchestrator fixes: checkout_book
+ ROW_COUNT type bug; pgTAP errcode; realtime pg_notify→realtime.send TODO (wave 4);
+ seed wiring in config.toml. CONTRACTS.md bumped to v1.1 (p_ arg prefix; Content-Profile
+ header). pgTAP suite authored but UNRUN (no Docker yet).
+- 6 Jul 2026 · 11-local-dev-stack · merged locally · Orchestrator fix: seed bcrypt hash was
+ invalid (verified with bcryptjs); replaced with a self-verified hash. Smoke script and
+ parity spike authored but UNRUN (no Docker yet); parity-check compiles clean.
+- 6 Jul 2026 (later) · runtime verification of 01+11 · direct commits · Full local stack
+ verified on Podman (not Docker Desktop). pgTAP 42/42; parity 4/4; smoke green; live RPC
+ round-trip OK. Fixes found by running: pgTAP plan count + RLS superuser-bypass (tests);
+ parity-check fabricated session token (MinIO validates tokens → DEV-CREDENTIALS spec
+ corrected to MinIO AssumeRole); smoke.ps1 PS-5.1 syntax/encoding bugs + JSON `supabase
+ status` parsing; compose lifecycle via `mc ilm rule add`; committed .gitkeep in all
+ bind-mounted dirs (Podman does not auto-create them).
diff --git a/Design/CloudTeamCollections/docs/unit-test-setup.md b/Design/CloudTeamCollections/docs/unit-test-setup.md
new file mode 100644
index 000000000000..f2dae6fcb907
--- /dev/null
+++ b/Design/CloudTeamCollections/docs/unit-test-setup.md
@@ -0,0 +1,45 @@
+# Cloud Team Collections — unit-test setup
+
+What a dev machine (or CI agent) needs to run the Cloud Team Collections *unit* tests, as
+opposed to the E2E harness (whose much longer requirements live in
+`src/BloomTests/e2e/README.md`).
+
+## The short version
+
+The default suites need **nothing beyond normal Bloom dev setup** (`./init.sh` once). They run
+mocked, with no containers, no local Supabase, no MinIO, no network, and no interactive
+desktop — safe for any ordinary CI agent, including one running as a service.
+
+| Suite | Command | Extra setup |
+|---|---|---|
+| C# cloud/TC unit tests (~334) | `dotnet test src/BloomTests/BloomTests.csproj --filter "(FullyQualifiedName~Cloud\|FullyQualifiedName~TeamCollection\|FullyQualifiedName~SharingApi)&FullyQualifiedName!~LiveTests"` | none |
+| Front-end component tests | ` cd src/BloomBrowserUI` then `yarn vitest run --pool=threads` | `yarn install` once |
+
+Notes that matter:
+- **Never pass `--no-build` to `dotnet test`** — a stale DLL can hide real regressions
+ (AGENTS.md rule). Building first is the point.
+- The C# filter above is the mandatory *widened* filter: `SharingApiTests` live under
+ `web.controllers` and match neither `~Cloud` nor `~TeamCollection`; a narrower filter once
+ let a real bug merge behind an "all green" claim.
+- vitest on Windows wants `--pool=threads` and single-run mode (`vitest run`, never watch);
+ the default fork pool has been seen timing out ("Timeout starting forks runner").
+
+## Suites that DO need the local dev stack
+
+Three opt-in suites talk to real services. They all need the local stack from
+`server/dev/README.md` (Supabase CLI + Podman/Docker + MinIO — the same "Machine setup"
+steps 2–4 in the E2E README, but NOT its unlocked-desktop requirement, since no Bloom.exe
+is launched):
+
+1. **C# live tests** — `CloudTeamCollectionLiveTests`, marked `[Explicit]`, excluded by the
+ default filter's `!~LiveTests`. Run deliberately with the stack up:
+ `dotnet test src/BloomTests/BloomTests.csproj --filter "FullyQualifiedName~CloudTeamCollectionLiveTests"`.
+2. **pgTAP schema/RLS tests** — `supabase test db` (42 tests; they `SET LOCAL ROLE
+ authenticated` because superuser bypasses RLS — a vacuously-green trap if ever rewritten).
+3. **Deno edge-function tests** — `deno test` under `supabase/functions/` (32 tests; these
+ mock S3/STS so they only need `deno`, not the stack — `npm i -g deno` or the standard
+ installer).
+
+A CI shape that works: the mocked suites in the ordinary per-commit build; the stack-backed
+suites (1–2) plus the E2E matrix on the dedicated interactive agent described in the E2E
+README's TeamCity section.
diff --git a/Design/CloudTeamCollections/docs/user-walkthrough.md b/Design/CloudTeamCollections/docs/user-walkthrough.md
new file mode 100644
index 000000000000..b22136296fb3
--- /dev/null
+++ b/Design/CloudTeamCollections/docs/user-walkthrough.md
@@ -0,0 +1,133 @@
+# Moving your Team Collection to the cloud
+
+*This page is the working source of truth for this walkthrough until it moves to the
+Bloom docs site. It covers migrating an existing folder-based Team Collection (for
+example, one shared over Dropbox or a network drive) to a cloud Team Collection, and
+inviting your team to it.*
+
+> **This feature is experimental.** You'll need to turn on "Cloud Team Collections
+> (experimental)" in Settings before any of the buttons below appear. Experimental
+> features can change or have rough edges. If you run into trouble, use Bloom's
+> **Report a Problem** button so the Bloom team can help.
+
+## Who this is for
+
+You already have a Team Collection that your team shares by putting a folder in
+Dropbox (or a similar shared/synced folder), and you want to switch to Bloom's new
+cloud-hosted Team Collections instead — no shared folder, no Dropbox account
+required for your team, and everyone just needs an internet connection and a Bloom
+account.
+
+## Before you begin: everyone checks in first
+
+**This step matters.** Before you disconnect your old Team Collection, make sure
+every team member has checked in all of their work, and that nobody has a book
+checked out. On the machine you'll use to create the new cloud collection (see
+below), open Bloom, go to the Collection tab, and check that every book shows as
+checked in (no lock icon).
+
+Why this matters: when you move to the cloud, Bloom copies whatever is in your
+*local* folder at that moment up to the cloud as the starting point for everyone.
+If a book had unsaved local edits that were never checked in to the old shared
+folder, that book's cloud copy won't have those edits. Checking in first (on every
+team member's machine, into the *old* Team Collection) makes sure nothing is lost.
+
+## Step 1: Turn on the experimental feature
+
+1. Open Bloom and go to **Settings > Advanced Settings**.
+2. Under **Experimental Features**, turn on **Cloud Team Collections (experimental)**.
+3. Restart Bloom when prompted.
+
+Do this on the computer of whoever will create the cloud collection. Each team
+member who wants to join later will also need to turn this on (once, on their own
+computer) before they can see or join a cloud Team Collection.
+
+## Step 2: Disconnect ("un-team") the old shared-folder collection
+
+Pick ONE computer to do this on — normally whoever manages the Team Collection.
+This step only needs to happen once, on one machine; other team members don't need
+to do anything to their own copies yet.
+
+1. Make sure step "Before you begin" above is done: everyone has checked in, and no
+ books are checked out.
+2. Open the collection in Bloom.
+3. Close Bloom.
+4. In your file manager, open this collection's folder (it's normally a
+ subfolder of Documents\Bloom, and shows a small Team Collection icon on
+ Bloom's own collection-chooser screen).
+5. Find the file named `TeamCollectionLink.txt` in that folder and delete it (or
+ move it somewhere else, in case you want it back).
+6. This collection is now an ordinary, un-shared local collection again. Your
+ original shared folder (in Dropbox or wherever it lived) is untouched — you can
+ clean it up later once your whole team has moved to the cloud.
+
+If you skip this step and try to enable cloud sharing anyway, Bloom will refuse
+with an error explaining that `TeamCollectionLink.txt` still links this collection
+to the old shared folder, and telling you to delete it first — so it's safe to try
+even if you're not sure whether this step already happened.
+
+## Step 3: Turn your local collection into a cloud Team Collection
+
+1. Open the (now un-teamed) collection in Bloom.
+2. Go to **Settings > Team Collection**.
+3. Click **Share this collection on the Bloom sharing server (experimental)**.
+4. Sign in with your Bloom account if you're not already signed in.
+5. Confirm the collection name — this can't be changed later, so make sure it's
+ right.
+6. Bloom uploads your books to the cloud. This can take a while for a large
+ collection; you'll see a progress bar. When it's done, you're the collection's
+ administrator.
+
+At this point, only you can see and use this cloud collection. The next step
+invites the rest of your team.
+
+## Step 4: Invite your team
+
+1. Still in **Settings > Team Collection**, you'll now see a list of people
+ approved to use this collection (just you, so far), with an option to add more.
+2. Enter each team member's email address and choose their role (**Member** or
+ **Administrator**). Use the email address they'll sign in to Bloom with.
+3. Repeat for everyone on your team.
+
+You don't need to invite people one at a time and wait — add everyone now, and
+they can each join whenever they're ready.
+
+## Step 5: Each team member joins
+
+Each invited team member does this, once, on their own computer:
+
+1. Turn on the **Cloud Team Collections (experimental)** feature, as in Step 1
+ above (if not already on), and restart Bloom.
+2. On Bloom's startup screen (the collection chooser), look for **Get my Team
+ Collections** on the right-hand side.
+3. Sign in with the same email address the administrator used to invite you.
+4. Your collection should appear in the list. Click it to pull it down.
+5. Bloom downloads a local copy and opens it automatically.
+
+### If you already have a local copy of this collection
+
+If you (or your team) had local copies of some of these books before switching to
+the cloud — for example, from before you un-teamed the old shared collection —
+Bloom will offer to merge your existing local collection with the cloud one instead
+of starting fresh, but this may replace divergent local copies of a book with
+whatever is in the cloud. This is one more reason the "everyone checks in first"
+step matters: it means everyone's local copy already matches what got uploaded to
+the cloud, so there's nothing to lose.
+
+## Troubleshooting
+
+- **"There is already a different Team Collection... on this computer"** — this
+ computer has a `TeamCollectionLink.txt` left over from Step 2 that wasn't fully
+ removed, or it's already linked to some other Team Collection with the same
+ name. Check the folder and remove or rename the conflicting collection folder,
+ then try again.
+- **The "Share this collection" button is disabled or missing** — check that
+ **Cloud Team Collections (experimental)** is turned on (Step 1) and that you've
+ restarted Bloom since turning it on.
+- **A team member can't see the collection under "Get my Team Collections"** —
+ double check the administrator invited the exact email address that person signs
+ in with, and that they've actually signed in (not just opened the sign-in
+ dialog).
+- Still stuck? Use Bloom's **Report a Problem** button — this feature is
+ experimental and the Bloom team wants to hear about anything that doesn't work
+ as described here.
diff --git a/Design/CloudTeamCollections/notes/write-book-status-audit.md b/Design/CloudTeamCollections/notes/write-book-status-audit.md
new file mode 100644
index 000000000000..eaa858300701
--- /dev/null
+++ b/Design/CloudTeamCollections/notes/write-book-status-audit.md
@@ -0,0 +1,132 @@
+# WriteBookStatus caller audit (task 00 prerequisite for task 05)
+
+`TeamCollection.WriteBookStatus(bookName, status)` writes a `BookStatus` to **both** the repo
+(via the abstract `WriteBookStatusJsonToRepo`) **and** the local status file. The cloud backend
+will need `WriteBookStatusJsonToRepo` to diff-dispatch to the narrowest RPC rather than always
+writing the full JSON blob.
+
+All callers are in `TeamCollection.cs` (abstract base class). None of the existing callers are
+in `FolderTeamCollection.cs` — that class only overrides `WriteBookStatusJsonToRepo`.
+
+---
+
+## Caller inventory
+
+### 1. `ForgetChangesCheckin` (line ~271)
+
+```csharp
+status = status.WithLockedBy(null);
+WriteBookStatus(finalBookName, status);
+```
+
+**What it writes**: Clears the lock (releases checkout) after abandoning local changes; the
+checksum is unchanged (it came from `GetLocalStatus` before restore).
+**Clears lock**: Yes — `WithLockedBy(null)`.
+**Cloud dispatch**: → `unlock_book` RPC (no content change).
+
+---
+
+### 2. `AttemptLock` — routed through `TryLockInRepo` (line ~717, post-task-00)
+
+```csharp
+TryLockInRepo(bookName, status); // status already has lockedBy set
+```
+
+**What it writes**: Sets `lockedBy`, `lockedByFirstName`, `lockedBySurname`, `lockedWhere`,
+`lockedWhen` on an otherwise-unchanged status.
+**Clears lock**: No — sets it.
+**Cloud dispatch**: → `checkout_book` RPC (conditional lock).
+
+---
+
+### 3. `UnlockBook` — routed through `UnlockInRepo(force:false)` (line ~697, post-task-00)
+
+```csharp
+WriteBookStatus(bookName, GetStatus(bookName).WithLockedBy(null));
+```
+
+**What it writes**: Clears the lock; all other fields unchanged.
+**Clears lock**: Yes.
+**Cloud dispatch**: → `unlock_book` RPC.
+
+---
+
+### 4. `ForceUnlock` — routed through `UnlockInRepo(force:true)` (line ~729, post-task-00)
+
+```csharp
+WriteBookStatus(bookName, GetStatus(bookName).WithLockedBy(null));
+```
+
+**What it writes**: Force-clears the lock (admin operation); all other fields unchanged.
+**Clears lock**: Yes.
+**Cloud dispatch**: → `force_unlock` RPC (audited; emits ForcedUnlock event).
+
+---
+
+### 5. `SyncAtStartup` — restore checkout (line ~2467)
+
+```csharp
+WriteBookStatus(bookName, localStatus);
+```
+
+**Context**: `localAndRepoChecksumsMatch && repoStatus.lockedBy == null`. Someone started a
+checkout remotely then changed their mind. We restore our checkout in the repo.
+**What it writes**: Restores the full local status (lock + checksum) to repo.
+**Clears lock**: No — re-asserts our lock.
+**Cloud dispatch**: → `checkout_book` RPC (re-assert our existing checkout).
+
+---
+
+### 6. `SyncAtStartup` — accept remote lock, no local edits (line ~2503)
+
+```csharp
+WriteBookStatus(bookName, repoStatus);
+```
+
+**Context**: `localAndRepoChecksumsMatch`, repo shows a different lock holder; local has no
+edits. We accept the repo's lock state.
+**What it writes**: Overwrites local status with repo status (changes lock owner).
+**Clears lock**: No (may set or change lock owner).
+**Cloud dispatch**: Local-only write — repo already has the correct state. Only
+`WriteLocalStatus` should be called; no repo RPC needed.
+
+---
+
+### 7. `SyncAtStartup` — update checksum after repo change, no local edits (line ~2527–2530)
+
+```csharp
+WriteBookStatus(bookName, localStatus.WithChecksum(repoStatus.checksum));
+```
+
+**Context**: Book changed in repo; local had no edits; we keep our local checkout but update
+the checksum to match the newly-downloaded repo version.
+**What it writes**: Updates the checksum in both repo and local status; lock unchanged.
+**Clears lock**: No.
+**Cloud dispatch**: Local-only write — the repo already has the new checksum (it IS the source
+of truth). Only `WriteLocalStatus` should be called; no repo RPC.
+
+---
+
+## Summary table
+
+| # | Call site | State written | Lock change | Cloud RPC |
+|---|-----------|---------------|-------------|-----------|
+| 1 | `ForgetChangesCheckin` | restore from repo + clear lock | clears | `unlock_book` |
+| 2 | `TryLockInRepo` (`AttemptLock`) | set lock fields | sets | `checkout_book` |
+| 3 | `UnlockInRepo(force:false)` (`UnlockBook`) | clear lock | clears | `unlock_book` |
+| 4 | `UnlockInRepo(force:true)` (`ForceUnlock`) | force-clear lock | clears | `force_unlock` |
+| 5 | `SyncAtStartup` — restore checkout | full local status | sets | `checkout_book` |
+| 6 | `SyncAtStartup` — accept remote lock | full repo status | changes owner | local-only |
+| 7 | `SyncAtStartup` — update checksum | checksum only | none | local-only |
+
+## Design note for task 05
+
+Callers 6 and 7 write to the repo despite the cloud already holding the authoritative state.
+For the cloud backend `WriteBookStatus` should be split so the repo half (`WriteBookStatusJsonToRepo`)
+becomes a no-op (or a thin diff) when the cloud is already up-to-date. The cleanest approach is
+to override `WriteBookStatus` in `CloudTeamCollection` and route each caller through the
+narrowest available RPC, falling back to a local-only write for cases 6 and 7.
+
+`ForgetChangesCheckin` (caller 1) currently reads the local status before restore; after the
+cloud book-copy lands in task 02+, this caller will need to signal an `unlock_book` RPC rather
+than going through the full `WriteBookStatus` path.
diff --git a/Design/CloudTeamCollections/orchestration/02-edge-functions.prompt.md b/Design/CloudTeamCollections/orchestration/02-edge-functions.prompt.md
new file mode 100644
index 000000000000..a686086ad10d
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/02-edge-functions.prompt.md
@@ -0,0 +1,46 @@
+# Agent prompt — task 02: edge functions (resume-aware)
+
+You are implementing task 02 of the Cloud Team Collections plan in an isolated git
+worktree of c:\github\BloomDesktop.
+
+**Resume check (do this FIRST):** if branch `task/02-edge-functions` exists, check it out
+(`git checkout task/02-edge-functions`), read the `## Progress log` at the bottom of
+`Design/CloudTeamCollections/tasks/02-edge-functions.md`, and continue from the recorded
+next action. Otherwise `git checkout -b task/02-edge-functions` (you start from the
+`cloud-collections` state).
+
+**Durability protocol (mandatory, from orchestration/RESUME.md):** commit after EVERY
+completed step — small coherent commits, descriptive messages ending
+"Co-Authored-By: Claude Fable 5 ". In the same commit: tick that
+step's checkbox in the task file AND update its `## Progress log` (create the section if
+missing) with `date · done · exact next action`. Never leave >1 step uncommitted.
+NOTE: the pre-commit hook may fail in a worktree ("husky-run not found"); if it does,
+commit with `--no-verify` (your files are SQL/TS/MD only — the hook is a JS/C# formatter;
+the orchestrator re-verifies at merge).
+
+**Read first:** `Design/CloudTeamCollections/tasks/02-edge-functions.md` (authoritative
+steps), `Design/CloudTeamCollections/CONTRACTS.md` (v1.1 — frozen shapes),
+`server/dev/DEV-CREDENTIALS.md` (CRITICAL correction: MinIO VALIDATES session tokens —
+dev credential mode must mint real temporary creds via MinIO's AssumeRole STS API, never
+fabricate a token), `server/dev/README.md`, and the schema/RPCs in `supabase/migrations/`.
+
+**Environment (all verified working on this machine):** local Supabase stack is running
+(`supabase start`: API http://127.0.0.1:54321, DB 54322; anon/service keys via
+`supabase status`). MinIO runs at http://localhost:9000 (bucket `bloom-teams-local`,
+versioning ON, creds minioadmin/minioadmin). Deno 2.9 is installed. Serve functions with
+`supabase functions serve`. Container networking wrinkle: from INSIDE the edge-runtime
+container, MinIO is not `localhost` — try `host.containers.internal` (Podman) or
+`host.docker.internal`; make the endpoint an env/secret (`BLOOM_S3_ENDPOINT`) and document
+what actually worked in server/dev/README.md.
+
+**Scope:** the five functions per CONTRACTS.md (checkin-start/finish/abort,
+download-start, collection-files-start/finish) under `supabase/functions/`, plus the
+`server/provision-aws` script (authored + reviewed only — no AWS account available; do
+not attempt to run it). Deno tests per function: unit tests with mocked S3/DB are fine
+(aws-sdk-client-mock is available on npm if useful for the JS AWS SDK), but where cheap,
+run integration checks against the LIVE local stack — it is up. Only these functions ever
+hold S3 admin credentials.
+
+**Final report (raw data):** branch + shas; per-function status (authored/tested, which
+tests ran vs mocked); the MinIO-endpoint networking answer; any contract ambiguities; the
+exact next action if you did not finish.
diff --git a/Design/CloudTeamCollections/orchestration/03-auth.prompt.md b/Design/CloudTeamCollections/orchestration/03-auth.prompt.md
new file mode 100644
index 000000000000..31cfee01aa59
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/03-auth.prompt.md
@@ -0,0 +1,43 @@
+# Agent prompt — task 03: auth + client skeleton (resume-aware)
+
+You are implementing task 03 of the Cloud Team Collections plan. Work in the MAIN working
+tree at c:\github\BloomDesktop (NOT a worktree — you need the initialized C# build deps).
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/03-auth` exists, check it out and continue from the `## Progress log` at the
+bottom of `Design/CloudTeamCollections/tasks/03-auth.md`. Otherwise
+`git checkout -b task/03-auth cloud-collections`.
+
+**Durability protocol (mandatory, from orchestration/RESUME.md):** commit after EVERY
+completed step — small coherent commits, descriptive messages ending
+"Co-Authored-By: Claude Fable 5 ". In the same commit: tick that
+step's checkbox in the task file AND update its `## Progress log` (create if missing)
+with `date · done · exact next action`. Never leave >1 step uncommitted.
+
+**Read first:** `Design/CloudTeamCollections/tasks/03-auth.md` (authoritative steps —
+note the dev-provider-first design), `Design/CloudTeamCollections/CONTRACTS.md` v1.1
+(wire format: RPC JSON keys use the `p_` prefix; `tc`-schema calls need
+`Content-Profile: tc` — both verified live), `server/dev/README.md` §Environment
+variables (the `BLOOM_CLOUDTC_*` contract `CloudEnvironment` must implement).
+
+**Environment:** local stack is running (Supabase API http://127.0.0.1:54321; anon key
+via `supabase status`). Seeded dev users: admin@dev.local / alice@dev.local /
+bob@dev.local, password BloomDev123! — sign-in verified working via
+POST /auth/v1/token?grant_type=password. Use these for any live verification of the dev
+auth provider.
+
+**Build caution:** Bloom.exe may be RUNNING on this machine. Building then fails copying
+the Bloom.exe apphost (MSB3027) — compilation still completes. Run tests with
+`dotnet test src/BloomTests/BloomTests.csproj --filter "FullyQualifiedName~Cloud"` (and
+the TeamCollection filter for regression); if the build errors ONLY on the apphost copy,
+verify output\Tests\Debug\AnyCPU\Bloom.dll is newer than your source changes and proceed
+with `dotnet test` on the built DLL, reporting exactly what you did. Never use stale
+binaries silently.
+
+**Scope (owns new files only):** `src/BloomExe/TeamCollection/Cloud/CloudEnvironment.cs`,
+`CloudAuth.cs`, `CloudCollectionClient.cs`, plus `src/BloomTests/TeamCollection/Cloud/`
+tests. If BloomExe.csproj needs explicit file includes, add ONLY your new files. All
+public methods commented. Editing a checked-out book must never block on auth.
+
+**Final report (raw data):** branch + shas; test commands + verbatim result counts; what
+was verified live vs unit-tested; the exact next action if you did not finish.
diff --git a/Design/CloudTeamCollections/orchestration/04-client-core.prompt.md b/Design/CloudTeamCollections/orchestration/04-client-core.prompt.md
new file mode 100644
index 000000000000..71ffff0558d5
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/04-client-core.prompt.md
@@ -0,0 +1,49 @@
+# Agent prompt — task 04: client core (cache, manifest, transfer) — resume-aware
+
+You are implementing task 04 of the Cloud Team Collections plan. Work in the MAIN working
+tree at c:\github\BloomDesktop (NOT a worktree — you need the initialized C# build deps).
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/04-client-core` exists, check it out and continue from the `## Progress log`
+at the bottom of `Design/CloudTeamCollections/tasks/04-client-core.md`. Otherwise
+`git checkout -b task/04-client-core cloud-collections`.
+
+**Durability protocol (mandatory, from orchestration/RESUME.md):** commit after EVERY
+completed step — small coherent commits, messages ending
+"Co-Authored-By: Claude Fable 5 ". Same commit: tick that step's
+checkbox in the task file AND update its `## Progress log` (create if missing) with
+`date · done · exact next action`. Never leave >1 step uncommitted — interruptions are
+EXPECTED and only commits survive.
+
+**Anti-hang rules:** no watch modes, no foreground servers; `--max-time` on curl; timeouts
+on anything that might block.
+
+**Read first:** `Design/CloudTeamCollections/tasks/04-client-core.md` (authoritative
+steps), `Design/CloudTeamCollections/CONTRACTS.md` v1.1 (S3 layout; manifest shape;
+book-status), the design doc's Architecture section, and the merged task-03 code in
+`src/BloomExe/TeamCollection/Cloud/` (build on `CloudCollectionClient.CallRpc` /
+`CallEdgeFunction`; config via `CloudEnvironment`).
+
+**Scope (owns new files only):** `src/BloomExe/TeamCollection/Cloud/CloudRepoCache.cs`,
+`BookVersionManifest.cs`, `CloudBookTransfer.cs` + tests in
+`src/BloomTests/TeamCollection/Cloud/`. Reuse `BloomS3Client` session-credential +
+TransferUtility mechanics (extract a shared helper if needed — do NOT disturb the publish
+path). Hard invariants: downloads ONLY by pinned (path, s3VersionId) — a test must assert
+no code path issues an unversioned GET; NFC path normalization; staged-temp-then-atomic-
+swap; junk-file exclusion reusing the publish path's filters. All public methods commented.
+
+**Environment:** the full local stack is UP (Supabase 127.0.0.1:54321; MinIO
+localhost:9000, bucket `bloom-teams-local`, minioadmin/minioadmin, versioning ON). Unit
+tests use mock S3 per the task file; optionally add [Explicit] live tests against MinIO
+(BasicAWSCredentials, ForcePathStyle, ServiceURL override).
+
+**Build caution:** Bloom.exe may be RUNNING. A build may fail ONLY on copying the
+Bloom.exe apphost (MSB3027) — compilation still completes. Run
+`dotnet test src/BloomTests/BloomTests.csproj --filter "FullyQualifiedName~Cloud"` plus the
+TeamCollection regression filter; if the apphost copy fails, verify
+output\Tests\Debug\AnyCPU\Bloom.dll is newer than your sources and report exactly what ran.
+Never silently use stale binaries.
+
+**Final report (raw data):** branch + shas; test commands + verbatim counts; the
+no-unversioned-GET assertion location; any base-class or contract issues found (report,
+don't fix); exact next action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/05-cloud-backend.prompt.md b/Design/CloudTeamCollections/orchestration/05-cloud-backend.prompt.md
new file mode 100644
index 000000000000..e0743968f261
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/05-cloud-backend.prompt.md
@@ -0,0 +1,76 @@
+# Agent prompt — task 05: CloudTeamCollection + monitor + join flow (resume-aware)
+
+You are implementing task 05 — the heart of the feature — in the MAIN working tree at
+c:\github\BloomDesktop (NOT a worktree; you need the initialized C# build deps).
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/05-cloud-backend` exists, check it out and continue from the `## Progress
+log` at the bottom of `Design/CloudTeamCollections/tasks/05-cloud-backend.md`. Otherwise
+`git checkout -b task/05-cloud-backend cloud-collections`.
+
+**Durability protocol (mandatory):** commit after EVERY completed step — this is the
+longest task in the plan and interruptions are CERTAIN. Small coherent commits, messages
+ending "Co-Authored-By: Claude Fable 5 "; same commit ticks the
+step's checkbox and updates the `## Progress log` with `date · done · exact next action`.
+
+**Anti-hang rules:** no watch modes or foreground servers; `--max-time` on curl; timeouts
+on anything that might block.
+
+**Read first (in this order):** the task file
+`Design/CloudTeamCollections/tasks/05-cloud-backend.md`;
+`Design/CloudTeamCollections/CONTRACTS.md` (v1.2 — note the NEW `get_book_manifest` RPC,
+added for exactly this task's Receive path);
+`Design/CloudTeamCollections.md` §Architecture + §Client integration;
+`Design/CloudTeamCollections/notes/write-book-status-audit.md` (task 00's caller audit —
+drives your WriteBookStatusJsonToRepo diff-dispatch);
+the merged Wave-1/2 code you build ON: `src/BloomExe/TeamCollection/Cloud/*.cs`
+(CloudEnvironment/CloudAuth/CloudCollectionClient from 03; CloudRepoCache/
+BookVersionManifest/CloudBookTransfer from 04) and the abstract members of
+`src/BloomExe/TeamCollection/TeamCollection.cs`.
+
+**File discipline (strict):**
+- You own NEW files: `Cloud/CloudTeamCollection.cs`, `Cloud/CloudCollectionMonitor.cs`,
+ `Cloud/CloudJoinFlow.cs` + tests under `src/BloomTests/TeamCollection/Cloud/`.
+- Base classes (`TeamCollection.cs`, `FolderTeamCollection.cs`, etc.) are READ-ONLY. If an
+ implementation genuinely needs a base change, STOP that step, record the need in your
+ progress log and final report, and work around it if possible — the orchestrator makes
+ base changes.
+- ONE authorized exception in `TeamCollectionManager.cs`: replace the two
+ NotImplementedException placeholders from task 00 (`CreateTeamCollectionFromLink`'s
+ cloud branch; `ConnectToCloudCollection`) with real CloudTeamCollection construction.
+ Touch NOTHING else in that file.
+
+**Key implementation constraints (from merged-work review notes):**
+- Receive: `get_book_manifest` RPC → CloudBookTransfer.DownloadFiles into a TEMP book
+ folder → atomic whole-directory swap done BY YOU (the transfer class's per-file move
+ loop is not itself a single atomic dir swap — merge log 7 Jul).
+- Locks: override TryLockInRepo/UnlockInRepo with checkout_book/unlock_book/force_unlock
+ RPCs (server stamps identity; client sends machine name only).
+- WriteBookStatusJsonToRepo: diff-dispatch to the NARROWEST RPC per the audit; pure
+ bookkeeping writes must never clear a lock; the SyncAtStartup callers flagged in the
+ audit are local-only no-ops for cloud.
+- RPC wire format: `p_`-prefixed JSON keys + Content-Profile/Accept-Profile: tc (already
+ handled inside CloudCollectionClient.CallRpc).
+- Monitor: polling only (get_changes, 60s + on-activation); event-id self-echo
+ suppression via last-seen cursor; realtime is a later wave.
+- Unified recovery (lock-lost/base-superseded): save `.bloomSource` to Lost & Found,
+ Receive current, log_event incident (type 100 WorkPreservedLocally), distinct messages
+ per sub-case.
+- All new user-visible strings: follow `.github/skills/xlf-strings/SKILL.md`, en-only.
+
+**Environment:** full local stack UP (Supabase 127.0.0.1:54321 — anon key via `supabase
+status`; MinIO via edge functions; dev users admin/alice/bob@dev.local pw BloomDev123!).
+For live integration tests: edge functions must be served — run
+`supabase functions serve --env-file server/dev/functions.env` via Start-Process/`&` with
+output redirected to a file, NEVER foreground (that stalled a prior agent). [Explicit]
+NUnit live tests against the stack are strongly encouraged for the Send/Receive round
+trip (see CloudAuthTests' LiveDevProvider test for the pattern).
+
+**Build caution:** Bloom.exe may be RUNNING; an apphost-copy MSB3027 during build is
+benign — verify output\Tests\Debug\AnyCPU\Bloom.dll is newer than your sources, run
+`dotnet test src/BloomTests/BloomTests.csproj --filter "FullyQualifiedName~Cloud"` and the
+`~TeamCollection` regression filter, and report exactly what ran. Never --no-build.
+
+**Final report (raw data):** branch + shas; test commands + verbatim counts (cloud filter,
+TC regression, any [Explicit] live runs); base-class changes you needed but could not
+make; contract ambiguities; exact next action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/06-api-endpoints.prompt.md b/Design/CloudTeamCollections/orchestration/06-api-endpoints.prompt.md
new file mode 100644
index 000000000000..5e263ee2cb6f
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/06-api-endpoints.prompt.md
@@ -0,0 +1,57 @@
+# Agent prompt — task 06: API endpoints (resume-aware)
+
+You are implementing task 06 in the MAIN working tree at c:\github\BloomDesktop (NOT a
+worktree). You are the exclusive owner of the shared file `TeamCollectionApi.cs` during
+this task.
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/06-api-endpoints` exists, check it out and continue from the `## Progress
+log` at the bottom of `Design/CloudTeamCollections/tasks/06-api-endpoints.md`. Otherwise
+`git checkout -b task/06-api-endpoints cloud-collections`.
+
+**Durability protocol (mandatory):** commit after EVERY completed step; small coherent
+commits ending "Co-Authored-By: Claude Fable 5 "; tick the step's
+checkbox + update the `## Progress log` (`date · done · exact next action`) in the same
+commit. Interruptions are expected; only commits survive.
+
+**Anti-hang rules:** no watch modes/foreground servers (background + redirect only);
+`--max-time` on curl; timeouts everywhere.
+
+**Read first:** `Design/CloudTeamCollections/tasks/06-api-endpoints.md` (authoritative
+steps); CONTRACTS.md v1.2 §Book-status JSON; the merged Cloud classes you expose
+(`CloudTeamCollection`, `CloudJoinFlow`, `CloudAuth.GetLoginState`,
+`CloudCollectionClient`); the END of
+`Design/CloudTeamCollections/tasks/08-ui-collection-tab.md`'s Progress log — it lists the
+~9 mocked endpoint names the Wave-2 UI shells already call (capabilities, tcStatusMetadata,
+sendAllBooks, receiveUpdates, sharing/forceUnlock, history events, etc.); your endpoints
+must match those names/shapes exactly, plus task 07's (sharing/loginState,
+sharing/showSignIn, collections/mine, collections/pullDown,
+teamCollection/showCreateCloudTeamCollectionDialog).
+
+**Scope:**
+- New `src/BloomExe/web/controllers/SharingApi.cs` (endpoints per the task file), thin
+ pass-throughs to CloudCollectionClient/CloudJoinFlow/CloudAuth — no business logic.
+- `TeamCollectionApi.cs`: additive only; book-status JSON gains
+ localVersionSeq/repoVersionSeq/signedIn/capability flags; receiveUpdates; sendAll;
+ force-unlock routes through the audited RPC for cloud. Folder-TC responses must stay
+ BYTE-IDENTICAL (existing tests untouched and green).
+- **One authorized supabase addition:** a NEW migration (never edit merged ones),
+ `supabase/migrations/20260707000006_tc_locked_by_display.sql`, extending
+ get_collection_state/get_changes/get_book_manifest outputs with `lockedByEmail` /
+ `lockedByName` (join tc.members on locked_by = user_id) — task 05 found locked_by is the
+ raw auth UUID, useless for "checked out to Sara" display. Apply with
+ `supabase migration up` and live-verify. Update the C# status JSON to carry them.
+- Websocket pushes for member-list changes + status refresh reuse existing contexts.
+
+**Environment:** local stack UP (Supabase 127.0.0.1:54321, anon key via `supabase
+status`; dev users admin/alice/bob@dev.local pw BloomDev123!; edge functions via
+`supabase functions serve --env-file server/dev/functions.env` in background w/ redirect).
+
+**Build caution:** Bloom.exe may be RUNNING; apphost-copy MSB3027 is benign — verify
+output\Tests\Debug\AnyCPU\Bloom.dll freshness, run
+`dotnet test src/BloomTests/BloomTests.csproj --filter "FullyQualifiedName~Cloud"`,
+`~TeamCollectionApi`, and the full `~TeamCollection` regression; report verbatim counts.
+
+**Final report (raw data):** branch + shas; endpoint list (name → implementation status →
+matched UI caller); test commands + verbatim counts; migration live-verification result;
+anything the UI wiring (next step) still needs; exact next action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/07-ui-setup.prompt.md b/Design/CloudTeamCollections/orchestration/07-ui-setup.prompt.md
new file mode 100644
index 000000000000..465452f2a7f7
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/07-ui-setup.prompt.md
@@ -0,0 +1,41 @@
+# Agent prompt — task 07: UI setup/sharing shells (resume-aware)
+
+You are implementing the Wave-1 (shells) scope of task 07 of the Cloud Team Collections
+plan in an isolated git worktree of c:\github\BloomDesktop.
+
+**Resume check (do this FIRST):** if branch `task/07-ui-setup` exists, check it out and
+continue from the `## Progress log` at the bottom of
+`Design/CloudTeamCollections/tasks/07-ui-setup.md`. Otherwise
+`git checkout -b task/07-ui-setup`.
+
+**Durability protocol (mandatory, from orchestration/RESUME.md):** commit after EVERY
+completed step — small coherent commits, descriptive messages ending
+"Co-Authored-By: Claude Fable 5 ". In the same commit: tick that
+step's checkbox in the task file AND update its `## Progress log` (create if missing)
+with `date · done · exact next action`. Never leave >1 step uncommitted.
+NOTE: the pre-commit hook may fail in a worktree ("husky-run not found"); if so, run
+`yarn prettier --write` on your changed files manually, then commit `--no-verify`
+(orchestrator re-verifies at merge).
+
+**Setup:** front-end lives in src/BloomBrowserUI. First run ` cd src/BloomBrowserUI &&
+yarn install` (yarn 1.22, NEVER npm; note the leading space guard for the terminal's
+lost-first-character quirk). NEVER run `yarn build`. Verify with `yarn lint` and vitest
+(component tests). Follow src/BloomBrowserUI/AGENTS.md: arrow-function components, no
+prop destructuring, @emotion/react `css` prop styling, no sx objects.
+
+**Read first:** `Design/CloudTeamCollections/tasks/07-ui-setup.md` (authoritative steps —
+Wave-1 scope is SHELLS AGAINST MOCKED ENDPOINTS; real wiring waits for task 06),
+`Design/CloudTeamCollections.md` §UI changes, CONTRACTS.md §Book-status JSON. In dev auth
+mode the sign-in step is a plain email/password form driven by `sharing/loginState`'s
+reported mode (mock that endpoint's both modes).
+
+**Localization:** every new user-visible string follows
+`.github/skills/xlf-strings/SKILL.md` (READ IT before adding strings). Only ever edit
+under `DistFiles/localization/en/` — never other language dirs.
+
+**Scope:** SharingPanel.tsx, JoinCloudCollectionDialog.tsx (new); CreateTeamCollection.tsx,
+TeamCollectionSettingsPanel.tsx, CollectionChooserDialog (exclusive owner during this
+task). Keep folder-TC behavior byte-identical; cloud paths behind the experimental flag.
+
+**Final report (raw data):** branch + shas; component list with test status; `yarn lint`
+result; strings added (XLF ids); the exact next action if you did not finish.
diff --git a/Design/CloudTeamCollections/orchestration/08-ui-collection-tab.prompt.md b/Design/CloudTeamCollections/orchestration/08-ui-collection-tab.prompt.md
new file mode 100644
index 000000000000..2f4439b0825b
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/08-ui-collection-tab.prompt.md
@@ -0,0 +1,45 @@
+# Agent prompt — task 08: UI collection tab (Wave-2 shells) — resume-aware
+
+You are implementing the Wave-2 (shells) scope of task 08 of the Cloud Team Collections
+plan in an isolated git worktree of c:\github\BloomDesktop.
+
+**Resume check (do this FIRST):** if branch `task/08-ui-collection-tab` exists, check it
+out and continue from the `## Progress log` at the bottom of
+`Design/CloudTeamCollections/tasks/08-ui-collection-tab.md`. Otherwise
+`git checkout -b task/08-ui-collection-tab`.
+
+**Durability protocol (mandatory, from orchestration/RESUME.md):** commit after EVERY
+completed step — small coherent commits, messages ending
+"Co-Authored-By: Claude Fable 5 ". Same commit: tick the step's
+checkbox in the task file AND update its `## Progress log` with
+`date · done · exact next action`. Interruptions are EXPECTED; only commits survive. The
+pre-commit hook fails in worktrees (husky-run): run `yarn prettier --write` on your changed
+files manually, then commit `--no-verify`.
+
+**Anti-hang rules:** vitest in single-run mode ONLY (`yarn vitest run ...`, never watch);
+never `yarn build`; no dev servers; timeouts on anything that might block.
+
+**Setup:** front-end lives in src/BloomBrowserUI. First run ` cd src/BloomBrowserUI &&
+yarn install` (yarn 1.22, NEVER npm; note the leading space — the terminal drops first
+characters). Follow src/BloomBrowserUI/AGENTS.md: arrow-function components, no prop
+destructuring, @emotion/react `css` prop, no sx objects.
+
+**Read first:** `Design/CloudTeamCollections/tasks/08-ui-collection-tab.md` (authoritative
+steps — Wave-2 scope is SHELLS AGAINST MOCKED ENDPOINTS; real wiring waits for task 06),
+CONTRACTS.md §Book-status JSON (StatusPanelState additions must stay in sync with the C#
+additive fields: localVersionSeq/repoVersionSeq/signedIn/capability flags), the design
+doc §UI changes, and merged task-07 patterns in src/BloomBrowserUI/teamCollection/
+(sharingApi.ts hooks; gating style).
+
+**GATING IS NON-NEGOTIABLE (a task-07 review caught an ungated section):** every visible
+cloud element added to EXISTING components (TeamCollectionButton, TeamCollectionDialog,
+TeamCollectionBookStatusPanel, statusPanelCommon, CollectionHistoryTable) must be behind
+the cloud-team-collections experimental feature / backend capability flags so folder-TC
+UI stays byte-identical with the flag off. Branch on capability, never concrete type.
+
+**Localization:** every new user-visible string follows
+`.github/skills/xlf-strings/SKILL.md` (read it); only edit `DistFiles/localization/en/`.
+
+**Final report (raw data):** branch + shas; component list with test status + verbatim
+counts; `yarn lint` result; XLF ids added; gating approach per component; exact next
+action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/09-e2e.prompt.md b/Design/CloudTeamCollections/orchestration/09-e2e.prompt.md
new file mode 100644
index 000000000000..7ed77ccd73ff
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/09-e2e.prompt.md
@@ -0,0 +1,60 @@
+# Agent prompt — task 09: E2E harness + scenarios (resume-aware)
+
+You are implementing task 09 in the MAIN working tree at c:\github\BloomDesktop (NOT a
+worktree — you build C# and launch real Bloom instances).
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/09-e2e` exists, check it out and continue from the `## Progress log` at the
+bottom of `Design/CloudTeamCollections/tasks/09-e2e.md`. Otherwise
+`git checkout -b task/09-e2e cloud-collections`.
+
+**Durability protocol (mandatory):** commit after EVERY completed step — harness pieces
+and each E2E scenario are separate commits, messages ending
+"Co-Authored-By: Claude Fable 5 "; tick checkboxes + progress log
+(`date · done · exact next action`) in the same commit. Interruptions are certain.
+
+**Read first:** `Design/CloudTeamCollections/tasks/09-e2e.md` (authoritative steps);
+`.github/skills/bloom-automation/SKILL.md` and `.claude/skills/run-bloom/SKILL.md` (CDP
+attach patterns, port discovery, kill gotchas); `server/dev/README.md` (stack, dev users,
+BLOOM_CLOUDTC_* env vars); the Wave-3 merge-log entry in IMPLEMENTATION.md (the 12 smoke
+bugs — several scenarios below exist to pin them).
+
+**HARD-WON ENVIRONMENT RULES (violating these wasted days during the smoke test):**
+1. NEVER run two `dotnet watch`/go.sh instances — concurrent rebuilds into the shared
+ output produce stale binaries. The harness design is: `dotnet build
+ src/BloomExe/BloomExe.csproj` ONCE per test session, then launch
+ `output\Debug\AnyCPU\Bloom.exe` directly N times with per-instance environment
+ (BLOOM_CLOUDTC_USER/_PASSWORD/_ANON_KEY, distinct collection folders, and Bloom's own
+ port auto-assignment handles HTTP/CDP). Launching the built exe is CORRECT here because
+ the harness itself just built it.
+2. Building while any Bloom.exe runs fails on the locked apphost — the harness must kill
+ all its instances before any rebuild, and must FAIL LOUDLY if a foreign Bloom.exe is
+ running at session start (tell the operator), never silently test stale code.
+3. Kill via the known-good pattern: killBloomProcess.mjs, then verify the port went dark
+ and Stop-Process any survivor PID (see the skill's gotchas; taskkill /PID is broken in
+ Git Bash).
+4. Anti-hang: servers/instances only in background with output redirected to files;
+ `--max-time` on curl; Playwright timeouts explicit; vitest/playwright single-run only.
+5. Per-test reset: `supabase db reset` replays migrations + seed (wipes tc data); clear
+ the MinIO bucket prefix via mc (see server/dev/docker-compose.yml patterns); delete
+ local test collection folders. The harness owns bringing the stack to a known state;
+ the experimental feature flag must be set in user.config BEFORE launching instances
+ (see the smoke-test hack recorded in IMPLEMENTATION.md merge log) — automate that.
+
+**Harness location:** new directory `src/BloomTests/e2e/` (Playwright over CDP, driven by
+`yarn playwright test` from src/BloomBrowserUI/react_components/component-tester or a
+self-contained package.json in the new dir — your choice, document it). TypeScript, no
+watch modes.
+
+**Scenario order (commit each; earlier ones unblock later ones):** E2E-1 create/share;
+E2E-2 two-instance collaboration loop (this automates the manual smoke); E2E-3 checkout
+contention; E2E-9 new-book lifecycle; E2E-5 approved accounts; E2E-7 un-team adoption;
+E2E-4 forced check-in recovery (this must REPRODUCE the latent recovery-path NRE noted in
+the merge log — fix it in CloudTeamCollection if your reproduction confirms it, that file
+is in scope for this fix only); E2E-6 kill mid-Send/resume; E2E-8 Receive-during-Send
+coherence; E2E-10 account-switch safety. If a scenario is blocked by a missing feature,
+document it in the progress log and move on rather than stalling.
+
+**Final report (raw data):** branch + shas; per-scenario status (green/blocked/why);
+harness invocation command; total wall time; bugs found (fixed vs reported); exact next
+action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/10-adoption.prompt.md b/Design/CloudTeamCollections/orchestration/10-adoption.prompt.md
new file mode 100644
index 000000000000..8baab3c8cb86
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/10-adoption.prompt.md
@@ -0,0 +1,57 @@
+# Agent prompt — task 10: adoption path + polish (resume-aware)
+
+You are implementing task 10 plus the Wave-3 polish list in an isolated git worktree of
+c:\github\BloomDesktop.
+
+**Resume check (do this FIRST):** if branch `task/10-adoption` exists, check it out and
+continue from the `## Progress log` at the bottom of
+`Design/CloudTeamCollections/tasks/10-adoption.md`. Otherwise
+`git checkout -b task/10-adoption`.
+
+**Durability protocol (mandatory):** commit after EVERY completed item, messages ending
+"Co-Authored-By: Claude Fable 5 "; tick checkboxes + progress log
+in the same commit. Worktree hook fails: `yarn prettier --write` your tsx/ts files, then
+`--no-verify`. For front-end tests: ` cd src/BloomBrowserUI && yarn install` first
+(leading space; yarn only; NEVER yarn build; vitest single-run only). C# can be AUTHORED
+here but not fully test-run (no build deps in worktrees) — write it carefully, note
+"authored, orchestrator verifies at merge" in the progress log, and keep C# changes in
+small isolated commits.
+
+**Read first:** `Design/CloudTeamCollections/tasks/10-adoption.md` (authoritative);
+IMPLEMENTATION.md's Wave-3 merge-log entry (the polish items below come from it);
+`.github/skills/xlf-strings/SKILL.md` (all strings en-only).
+
+**Work items, in order:**
+1. **Proper experimental-feature checkbox** (owed since the smoke test's user.config
+ hack): add "Cloud Team Collections (experimental)" to Settings → Advanced, wired like
+ the existing allowTeamCollection option end to end — GetAdvancedSettingsData /
+ StoreAdvancedSettingsData / CollectionSettingsDialog pending-change + restart plumbing
+ in `CollectionSettingsApi.cs`, checkbox in `AdvancedSettingsPanel.tsx`, token
+ `ExperimentalFeatures.kCloudTeamCollections`. Component test for the tsx side.
+2. **Pull-down auto-open**: `collections/pullDown` (SharingApi.cs) returns the local
+ collection folder path from CloudJoinFlow; the chooser/join dialog uses it to invoke
+ the same open-collection action the chooser's cards use, instead of making the user
+ hunt for the new collection. Component test.
+3. **Un-team cleanup** (task file step 1): enabling cloud on a formerly-folder-TC
+ collection cleans per-book `TeamCollection.status`, `lastCollectionFileSyncData.txt`,
+ `log.txt`; simultaneous folder-link + cloud-link in TeamCollectionLink.txt territory =
+ clear error. Unit-testable C# — author tests alongside.
+4. **First-Receive reconcile** (task file step 2): verify-by-reading that members' existing
+ local copies reconcile by checksum on first Receive (CloudJoinFlow's scenario logic);
+ document findings in the progress log; fix only if trivially wrong.
+5. **User documentation**: author `Design/CloudTeamCollections/docs/user-walkthrough.md` —
+ the un-team → enable cloud → invite team walkthrough incl. "everyone check in first",
+ written for end users (the docs-site source of truth until it moves).
+6. **Localization sweep**: audit ALL new user-visible strings across the cloud UI work
+ (07/08/wiring/your items) per the xlf-strings skill; fix gaps; en-only.
+7. **Analytics audit**: verify create/join/send/receive/force-unlock/incident events carry
+ Backend="Cloud" and sensible params (read TeamCollectionApi/SharingApi Analytics.Track
+ calls); add missing ones; note bytes-uploaded-vs-skipped as future enhancement if not
+ cheaply available.
+
+NOT in scope: the preview-pane refresh nit (needs base-code selection plumbing — the
+orchestrator will assess separately); dogfood (humans).
+
+**Final report (raw data):** branch + shas; per-item status; test commands + verbatim
+counts (front-end); which C# items need orchestrator build-verification; XLF ids
+added/fixed; exact next action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/12-real-auth.prompt.md b/Design/CloudTeamCollections/orchestration/12-real-auth.prompt.md
new file mode 100644
index 000000000000..3d879815472a
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/12-real-auth.prompt.md
@@ -0,0 +1,39 @@
+# Agent prompt — task 12: real auth provider, Option A seams (resume-aware)
+
+You are implementing task 12 in the MAIN working tree at c:\github\BloomDesktop (you build
+and unit-test C#; you must NOT launch Bloom.exe or run E2E tests — the machine is in
+interactive use by the developer).
+
+**Resume check (do this FIRST):** `git status` must be clean (stop and report if not). If
+branch `task/12-real-auth` exists, check it out and continue from the `## Progress log` at
+the bottom of `Design/CloudTeamCollections/tasks/12-real-auth.md`. Otherwise
+`git checkout -b task/12-real-auth cloud-collections`.
+
+**Durability protocol (mandatory):** commit after EVERY completed step, messages ending
+"Co-Authored-By: Claude Fable 5 "; tick checkboxes + progress log
+(`date · done · exact next action`) in the same commit. Interruptions are likely.
+
+**Read first:** `Design/CloudTeamCollections/tasks/12-real-auth.md` (authoritative steps);
+`Design/CloudTeamCollections/GOING-LIVE.md` Phase 3 (the boundary between your work and the
+deferred live wiring); the existing dev provider + seam in
+`src/BloomExe/TeamCollection/Cloud/` (CloudAuth.cs, CloudEnvironment.cs) and its tests in
+`src/BloomTests/TeamCollection/Cloud/`; how Bloom currently hosts BloomLibrary sign-in
+(search WebLibraryIntegration and the sharing sign-in flow) BEFORE designing the token
+receipt endpoint; `supabase/migrations/` for tc.jwt_email_verified()'s current shape;
+`.github/skills/xlf-strings/SKILL.md` if you add any user-visible strings (en-only;
+NEVER a double hyphen inside a — it crashes every Bloom launch).
+
+**Test rules:** the mandatory C# filter is
+`"(FullyQualifiedName~Cloud|FullyQualifiedName~TeamCollection|FullyQualifiedName~SharingApi)&FullyQualifiedName!~LiveTests"`;
+never `--no-build`; never `yarn build`; vitest (if any front-end) single-run with
+`--pool=threads`. pgTAP (if you add migrations): `supabase test db` with the stack up — it
+already is; `supabase db reset` is allowed. All HTTP in unit tests is mocked; no live
+Google/Firebase calls anywhere.
+
+**Contract discipline:** document the token-receipt request shape in CONTRACTS.md (new
+"Auth (Option A)" section) — the BloomLibrary2 editor.ts change will be written against
+your text verbatim, so be precise about route, method, body fields, and the reply.
+
+**Final report (raw data):** branch + shas; per-step status; test commands + verbatim
+counts; the CONTRACTS.md section text you added; anything you discovered about the existing
+BloomLibrary sign-in flow that affects GOING-LIVE Phase 3.2; exact next action if unfinished.
diff --git a/Design/CloudTeamCollections/orchestration/BUG0-OPTION-A-SKETCH.md b/Design/CloudTeamCollections/orchestration/BUG0-OPTION-A-SKETCH.md
new file mode 100644
index 000000000000..e323d5ac75e3
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/BUG0-OPTION-A-SKETCH.md
@@ -0,0 +1,48 @@
+# Bug #0 option (a) implementation sketch — server-side "seat"
+
+Prepared while the bot gauntlet runs; NOT committed anywhere. If John picks (a), this is the plan.
+
+## Concept
+A "seat" = one local collection folder on one machine. Lock takeover is only legitimate within
+the same seat (the true shared-computer scenario: account B opens the exact folder account A
+used). Two folders on one machine are two seats.
+
+## Server (one migration, purely additive like 20260709000007)
+- `alter table tc.books add column locked_seat text;` (nullable; null = legacy/unknown seat).
+- `checkout_book(p_book_id, p_machine, p_seat)`: new optional param, stored on lock acquire.
+ (PostgREST tolerates the extra param only if the SQL function signature adds it — bump the
+ function, keep old 2-arg overload delegating with p_seat=null so old clients don't break;
+ CONTRACTS.md addition to note.)
+- `checkout_book_takeover(p_book_id, p_machine, p_seat)`: takeover requires
+ `locked_by_machine = p_machine AND locked_seat IS NOT DISTINCT FROM p_seat AND locked_seat IS NOT NULL`
+ — i.e. seat must match AND be known; a null (legacy) seat refuses takeover (fail safe).
+- `unlock_book` / `force_unlock` / `checkin_finish_tx`: clear locked_seat wherever locked_by is
+ cleared (audit which already clear locked_by_machine; mirror that).
+- pgTAP: same-seat takeover OK; same-machine-different-seat REFUSED; null-seat REFUSED;
+ cross-machine REFUSED (existing); checkout stores seat; unlock clears it.
+
+## Client
+- Seat id: stable hash of the local collection folder path + machine
+ (e.g. first 16 hex of SHA256(lowercased full path)). Compute in TeamCollectionManager or
+ CloudTeamCollection (has _localCollectionFolder). NOT the raw path (privacy in server rows).
+- CloudCollectionClient.CheckoutBook/CheckoutBookTakeover: add seat param.
+- CloudTeamCollection.TryLockInRepo / TryTakeOverLock: pass the seat.
+- CanTakeOverLockOnThisMachine: unchanged machine check client-side (server enforces seat);
+ optionally ALSO gate client-side if the cache carries locked_seat (cache delta shape would
+ need the column too — get_collection_state view addition).
+- Cache: add locked_seat to CloudRepoCache book rows + snapshot/delta parsing (or skip caching
+ it and let the server be sole enforcer — SIMPLER: skip cache change; client attempts
+ takeover, server refuses, AttemptLock then correctly reports "locked by other").
+ RECOMMENDED: skip the cache/client gate entirely; server-only enforcement. Client behavior
+ on refusal already falls back to the ordinary "locked by someone else" path (verify
+ TryTakeOverLock's failure handling does this — it should treat {success:false} like a lost
+ checkout race).
+
+## e2e-4 expectation
+With (a): alice's attemptLock → takeover refused (different seat) → lock stays bob's →
+spec's final assertions pass unchanged. e2e-10's takeover (same folder = same seat) keeps
+passing (its bob-takeover opens ALICE's folder).
+
+## Estimate
+Migration + pgTAP ~1h careful work incl. running against local stack; client param plumbing
+~20 min; e2e-4 + e2e-10 rerun ~10 min.
diff --git a/Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md b/Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md
new file mode 100644
index 000000000000..d15f4d858c84
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md
@@ -0,0 +1,948 @@
+# Dogfood batch 1 (9 Jul 2026) — restartable work plan
+
+John's bug/improvement list from first real dogfooding, plus decisions already made.
+This file is the durable state for the batch: the orchestrator ticks checkboxes and
+updates each item's `Status:` line as work proceeds (same protocol as RESUME.md — commit
+after every completed step, progress state lives in git, never only in a conversation).
+
+**To restart after any interruption:** start a fresh Claude Code session in this repo and
+say: **"Resume the dogfood batch per
+Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md."** The resumer reads each
+item's Status line, secures any uncommitted `task/*` or worktree work as WIP commits, and
+continues with the next unchecked step. General orchestration rules (review-before-merge,
+C# test filter, dev-stack bring-up, environment quirks) are in RESUME.md and apply here.
+
+**Testing protocol for this batch (agreed with John 9 Jul):** per item, run only the 1–2
+E2E specs covering the touched area (each spec independently wipes + reseeds the dev DB,
+so subsets are trustworthy); add `e2e-1-create-share` whenever a change touches startup or
+localizable strings. One full-matrix run before pushing the finished batch. E2E runs are
+serialized (shared dev DB + real Bloom launches); code work may proceed during a run.
+Bloom windows go to John's spare screen via machine-level `BLOOM_E2E_SCREEN=1`.
+
+**Decision already made (9 Jul, John):** remote-checkin application is FULLY AUTOMATIC —
+when the poll notices a checkin for a book not checked out here, apply it to the local
+book folder immediately and refresh the preview if that book is selected. The Sync button
+(renamed from Reload) forces an immediate pass; no updates-available nag for the common
+case.
+
+## Work order
+
+Chosen order: quick wins first, then the propagation cluster (items 4+5 are one work
+item), then the two larger UI features. Items 1–3 are independent of everything else.
+
+### 1. "Bloom is busy" missing localization `[quick]`
+Status: DONE (commit 2d74d280f; e2e-1 GREEN in the 9 Jul PM 4-spec queue)
+- [x] Found: ExternalBusyOverlay.tsx's fallback message already had id Common.BloomIsBusy
+ but no XLF entry; the useL10n lookup logged the complaint on every collection-tab
+ mount. (The specific BloomBridge message the overlay usually shows is intentionally
+ unlocalized; only the fallback needed an entry.)
+- [x] Added to BloomLowPriority.xlf (John's choice) with translate="no" + context note.
+- [x] Verify: `e2e-1-create-share` GREEN (9 Jul PM 4-spec queue, 4/4 in 9.5 min). An
+ earlier attempt failed ONLY because the desktop locked mid-run (WebView2 stuck at
+ about:blank — the known signature).
+
+### 2. Poll immediately on book selection `[quick]`
+Status: DONE (commit 6f0c4a068; e2e-2 GREEN in the 9 Jul PM queue)
+- [x] TeamCollectionManager ctor now subscribes BookSelection.SelectionChanged → if the
+ current collection is a CloudTeamCollection, Task.Run(PollNow). Results flow through
+ the existing change-event pipeline (same as timer polls).
+- [x] Guard: PollNow's own in-flight coalescing covers rapid selection changes; null
+ bookSelection guard for unit-test constructions (caught by test run: 10 failures,
+ fixed, 363/363 green).
+- [x] `e2e-2-collaboration-loop` GREEN (9 Jul PM queue) (note: E2E uses a 5s poll, so the
+ speedup itself is mostly invisible there — the run guards against regressions; the
+ real check is John's manual test at the default 60s poll).
+
+### 3. Center the checkin-progress dialog in the status panel `[quick]`
+Status: CODE DONE + e2e-2 GREEN — remaining: John's VISUAL check of the centered dialog
+during his next manual checkin
+- [x] It's the React BloomDialog in TeamCollectionBookStatusPanel (not BrowserProgressDialog):
+ now positioned via PaperProps over the #teamCollection div's center, vertically
+ clamped so the paper stays on-screen (the panel hugs the window bottom). Falls back
+ to default whole-window centering when #teamCollection is absent (unit tests).
+ Panel vitest suite 11/11.
+- [x] `e2e-2-collaboration-loop` GREEN (9 Jul PM queue).
+- [ ] [HUMAN, John] Visual check that the checkin-progress dialog appears centered over the
+ status panel during a manual checkin.
+
+### 4+5. Automatic remote-update application + in-place Sync (one work item) `[medium]`
+Status: MERGED + E2E VERIFIED (9 Jul PM queue: e2e-2 + e2e-8 GREEN with auto-apply active;
+earlier: orchestrator re-ran C# 375/375 + both vitest files green, reviewed
+queue/wiring/panel/XLF line by line). Residual risk (checkout racing the download window)
+did not surface in the contention-heavy e2e-8 run; keep an eye on it in the pre-push full
+matrix.
+Observed bug: after a remote checkin, the other instance updated lock state (avatar +
+status panel) but the TC button showed no "updates available" and the preview did not
+refresh; book folder content update unverified.
+- [x] Diagnose: CloudCollectionMonitor.OnPolledChanges DOES already notify the base
+ TeamCollection change pipeline correctly (RaiseBookStateChange → HandleModifiedFile at
+ Application.Idle), and it DID reach the HasBeenChangedRemotely branch — but that branch
+ only ever wrote a "TeamCollection.BookModifiedRemotely" NewStuff log message; it never
+ copied anything to the local folder (confirmed: folder TC has the exact same message-only
+ behavior for this branch, so this wasn't a cloud-specific regression, just a gap neither
+ backend had filled in). Root cause of "TC button showed no updates available": the
+ top-bar TeamCollectionButton's color/label state comes from `teamCollection/tcStatus`
+ (CollectionTabView.cs sends this from TeamCollectionMessageLog.TeamCollectionStatus, which
+ is driven ONLY by whether a NewStuff message log entry exists) — it is a SEPARATE signal
+ from `teamCollection/tcStatusMetadata`'s `updatesAvailableCount` (which reads the
+ CloudRepoCache version numbers directly and is pushed via the `statusMetadataChanged`
+ socket event on every poll). So `updatesAvailableCount` was almost certainly already
+ correct and live; what was missing was confirming a NewStuff message actually got written
+ for John's test run (a corrupted/stuck session, a race, or simply that this was the very
+ poll that revealed the gap) — this needs re-verification once the desktop is available,
+ but the code-level cause (message-only branch, no auto-apply) is confirmed and is exactly
+ what this item's implementation now fixes.
+- [x] Implement fully-automatic application: TeamCollection.CanAutoApplyRemoteChanges (false by
+ default; true only for CloudTeamCollection) + a new single-consumer RemoteBookAutoApplyQueue
+ (dedupes by book name, one book at a time, runs on Task.Run so downloads never block the UI
+ thread). HandleModifiedFile's HasBeenChangedRemotely branch now queues the book instead of
+ just logging when CanAutoApplyRemoteChanges is true; the worker re-verifies eligibility
+ (still changed remotely, not checked out here, no clobber/checkout conflict) at the moment
+ it actually runs, then reuses CopyBookFromRepoToLocal, updates book status, and refreshes
+ the preview (SendBookContentReload) only if the applied book is the one currently selected.
+ Falls back to exactly the old message-only behavior on failure or ineligibility. Folder TCs
+ are unaffected (CanAutoApplyRemoteChanges stays false there).
+- [x] SAFETY (John, 9 Jul): CopyBookFromRepoToLocal already staged-then-atomically-swapped before
+ this task (two directory renames; not reimplemented). The auto-apply worker's
+ re-verification (checked-out-here / clobber / checkout-conflict, re-read fresh on the
+ worker thread right before copying) is the mechanism that keeps a book "busy-safe": if the
+ user checks it out, starts editing, or otherwise changes its state between queueing and the
+ worker actually running, the worker backs off silently rather than clobbering anything.
+ NOT separately implemented: an explicit UI-level "busy" lock during the download window
+ itself (publish/delete/rename aren't specially blocked while a download is in flight) —
+ the re-verification step covers the has-this-changed-since-queueing race but a reviewer
+ should double check there's no narrow window between the re-verification check and the
+ actual folder swap where a concurrent user action could interleave badly. Given the swap
+ itself is two fast directory renames (not file-by-file writes), this window is believed
+ negligible but wasn't independently stress-tested.
+- [x] Rename "Reload" to "Sync" for cloud TCs. The existing `teamCollection/receiveUpdates`
+ backend endpoint already did PollNow() before its receive loop (no server-side change
+ needed) — only the label changed, in both the dialog and the per-book status panel.
+ Discovered and fixed a related bug while in there: the per-book panel's generic
+ "needsReload" state (isChangedRemotely, a content-update signal identical for both
+ backends) was checked ahead of the cloud-specific "updatesAvailable" state, so a cloud
+ book with a pending remote change showed "Reload Collection" instead of "Sync" — cloud now
+ renders needsReload with the same copy/button as updatesAvailable; folder TCs (and genuine
+ hasConflictingChange for either backend) are unchanged.
+- [x] Keep the reload-requiring paths (settings changes etc.) working — HandleCollectionSettingsChange
+ and the dialog's showReloadButton/"Reload Collection" path were not touched at all.
+- [x] XLF for the new "Sync" label: renamed TeamCollection.ReceiveUpdates → TeamCollection.Sync
+ (was translate="no", so free to rename per the xlf skill) and updated
+ UpdatesAvailableForBookDescription's text/note to match.
+- [ ] Verify: `e2e-2-collaboration-loop` + `e2e-8-receive-during-send`; e2e-1 for the XLF. NOT
+ run — this task's hard rules forbid launching Bloom / running e2e (desktop session
+ locked); queued for the next E2E pass alongside items 1–3.
+- [x] Update tests/specs that assert on the old Reload wording/behavior: TeamCollectionDialog.test.tsx
+ and TeamCollectionBookStatusPanel.test.tsx updated for the rename, plus new tests for the
+ auto-apply eligibility logic (RemoteBookAutoApplyQueueTests, TeamCollectionAutoApplyTests)
+ and the needsReload cloud/folder split. C# required filter 375/375 green; both touched
+ vitest files green (5/5, 13/13); yarn typecheck and eslint clean.
+
+### 6. Join-card integration in the collection chooser `[medium]`
+Status: CODE DONE on branch `task/b1-6-join-cards` (288e4057e, 9fe1c4de4, 8a3a01130,
+f59dc6ea3, 5e572a84d; not yet merged into cloud-collections) — E2E verification QUEUED
+(desktop locked; forbidden by this task's rules)
+- [x] In the collection chooser dialog, remove the separate "team collections to join"
+ list; instead add extra cards to the MAIN collection list for collections the user
+ is invited to (server membership exists) but has no local copy of.
+ MyCloudCollectionsSection.tsx + its test deleted; CollectionChooser now fetches the
+ new `collections/getJoinCards` endpoint and passes results to CollectionCardList.
+- [x] NO join card when the user already joined + has a local copy. DO show a join card
+ when a same-named local collection exists that is NOT a TC (existing join-conflict
+ code handles the actual join). CollectionChooserApi.ComputeJoinCards matches by
+ cloud collection id ONLY (via TeamCollectionLink.txt scan of MRU + discovered local
+ folders, GetLocalCloudCollectionIds) -- a same-named non-cloud-linked local folder
+ still gets a join card; CloudJoinFlow's own scenario matching resolves merge-or-
+ conflict once the user actually tries to join, unchanged.
+- [x] Join cards do not count against the MRU-list card limit. CollectionCardList slices
+ `collections` at maxCardCount=10 first, then appends `joinCollections` (unsliced).
+- [x] Omit any card info not available for an unjoined TC (thumbnail, languages, …).
+ CollectionCard's isJoinCard variant shows only title + TeamCollectionIcon + a "Get"
+ join cue (reusing CollectionChooser.PullDown's wording); no per-card fetch (the
+ unpublished-count effect is skipped) and the "..." Show-in-Explorer menu is hidden
+ (no local folder exists yet).
+- [ ] Verify: `join-auto-open` + `e2e-1-create-share`; vitest for the card-list logic. The
+ vitest half is DONE (CollectionCardList.test.tsx: 4/4; CollectionChooser.test.tsx
+ rewritten: 3/3) -- only the E2E launches remain queued (desktop locked; this task's
+ hard rules forbid launching Bloom/e2e). `join-auto-open.spec.ts` exists under
+ src/BloomTests/e2e/tests; checked its content -- it drives collections/pullDown and
+ workspace/openCollection directly via HTTP (not through any chooser UI selector), so
+ it does not touch MyCloudCollectionsSection/join cards at all and should be
+ unaffected by this change. Still queued to actually run (desktop locked) as a
+ regression check, per this task's hard rules.
+
+Implementation notes (scouted 9 Jul, read-only — verified paths/lines):
+- Chooser: `collection/CollectionChooserDialog.tsx` wraps `CollectionChooser.tsx` (MRU via
+ `collections/getMostRecentlyUsedCollections`; cloud list via `useMyCloudCollections` →
+ GET `collections/mine`; `joinTarget` state opens `JoinCloudCollectionDialog`).
+- The separate list to REMOVE: `collection/MyCloudCollectionsSection.tsx` (+ its test);
+ its "Get" button → `pullDownCollection` → POST `collections/pullDown` →
+ `SharingApi.HandlePullDown` → `CloudJoinFlow.JoinCollection` (keep all of that; join
+ cards reuse `JoinCloudCollectionDialog` and the pull-down + auto-open flow unchanged).
+- MRU card cap: `CollectionCardList.tsx` `maxCardCount = 10` slice — join cards must be
+ appended AFTER the slice. Card shape: `ICollectionInfo` in `CollectionCard.tsx`
+ (path/title/bookCount/checkedOutCount/unpublishedCount/isTeamCollection) — join-card
+ variant needs `collectionId`, a join flag, minimal info, no per-card unpublished fetch.
+- "Has local copy of cloud collection X": for each MRU/local folder,
+ `TeamCollectionManager.GetTcLinkPathFromLcPath` + `TeamCollectionLink.FromFile`
+ (`IsCloud`, `CloudCollectionId`); a summary from `collections/mine` gets a join card iff
+ no local cloud link matches its id. `CloudJoinFlow.DetermineScenario` (CloudJoinFlow.cs
+ ~128) already does this matching — reference/reuse. Same-name non-TC local collections
+ STILL get a join card (CloudJoinFlow's PlainCollectionSameGuid/DifferentGuid handles the
+ merge-or-error; the card test keys off cloud links ONLY, not names).
+- Server-side merge preferred: extend `CollectionChooserApi` (application-level, like
+ SharingApi) with an `internal static` pure matching helper (SharingApiTests' pattern —
+ no CollectionChooserApiTests file exists yet).
+- Tests to rewrite: `CollectionChooser.test.tsx`; delete `MyCloudCollectionsSection.test.tsx`
+ with its component; `JoinCloudCollectionDialog.test.tsx` stays valid.
+
+### 7. Progressive join: open the collection before all books download `[large]`
+Status: CODE DONE on branch `task/b1-7-progressive-join` (created from origin/cloud-collections;
+not yet merged) — E2E verification QUEUED (orchestrator's job per this task's hard rules)
+- [x] On join, fetch collection settings + book list (titles) first, open the collection
+ immediately; books not yet downloaded show a placeholder icon suggesting an
+ in-progress download. CloudJoinFlow.JoinCollection: removed the blocking
+ CopyAllBooksFromRepoToLocalFolder call; every repo book is now queued via the new
+ TeamCollection.QueueBookForBackgroundDownload right after settings download.
+ CollectionApi.HandleBooksRequest merges CloudTeamCollection's repo book list against local
+ folders (new pure ComputeNotYetDownloadedBookEntries + BookListEntry DTO) so repo-only
+ books appear with `notYetDownloaded: true`; BookButton.tsx renders these as a simple
+ dashed-border placeholder (cloud-download icon + title, no thumbnail request) instead of
+ the normal interactive button.
+- [x] Background-download books; swap each placeholder for the real icon as its download
+ completes. New TeamCollection.DownloadMissingBookInBackground (the RemoteBookAutoApplyQueue
+ worker's new branch for books with no local folder at all) downloads, updates status, then
+ invalidates the cached book list and sends the existing "editableCollectionList"/"reload:"
+ websocket event so BooksOfCollection.tsx's collections/books re-fetch swaps the placeholder
+ for the real button automatically.
+- [x] Selecting a not-yet-downloaded book bumps it to the front of the download queue; status
+ panel shows a "downloading" message until it arrives. RemoteBookAutoApplyQueue gained
+ EnqueueFront (priority, dedupe-preserving, never interrupts an in-flight download);
+ CollectionApi's selected-book POST handler gracefully detects a placeholder click
+ (TryPrioritizeNotYetDownloadedBook, via new CloudTeamCollection.TryGetBookNameForInstanceId
+ + PrioritizeDownload) and bumps it to the front instead of crashing on the missing
+ BookInfo. DEVIATION (flagged for John/orchestrator): the "downloading" indicator is shown
+ as a persistent placeholder icon on the book button itself (visible for every
+ not-yet-downloaded book) rather than routing through the real BookSelection/preview-pane
+ and TeamCollectionBookStatusPanel.tsx's StatusPanelState union, per the scout notes' exact
+ seam — faking a "selected" placeholder book (no real local folder/Book object exists yet)
+ risked destabilizing the preview iframe and the lock/checkout endpoints that also key off
+ BookSelection.CurrentSelection. The functionally important, tested part (priority bump) IS
+ implemented; only the panel-specific visual treatment was simplified.
+- [x] Same SAFETY rule as item 4+5: a book appears in the collection only as placeholder
+ (no dangerous actions possible) or fully downloaded — never as a half-populated folder the
+ user can act on. Temp-folder staging + atomic swap. The placeholder branch in BookButton.tsx
+ is a completely separate render path with no context menu, no rename, no double-click-edit;
+ the only action is a click that posts to collections/selected-book (priority bump, graceful,
+ never a real selection). CopyBookFromRepoToLocal's existing stage-then-atomic-swap (unchanged)
+ still guarantees a book is placeholder-only or fully downloaded, never half-populated.
+- [x] Handle interruption: Bloom closed mid-join resumes/completes downloads on next open
+ (SyncAtStartup should already fetch missing books — verify). Verified AND changed: cloud
+ SyncAtStartup's "brand new book!" branch now reroutes to the same background queue
+ (QueueBookForBackgroundDownload) instead of fetching synchronously inline, when
+ CanAutoApplyRemoteChanges is true (cloud only) — so a half-joined collection's next open
+ stays fast and downloads keep resuming in the background, instead of blocking the startup
+ sync dialog on every still-missing book. Folder TCs are completely unaffected (unchanged
+ synchronous fetch, pinned by a new regression test).
+- [ ] Verify: `join-auto-open` + `e2e-9-new-book-lifecycle`; consider a new spec for the
+ placeholder/priority behavior if cheap. NOT run — this task's hard rules forbid launching
+ Bloom/e2e (orchestrator's job after merge, serialized with other E2E runs).
+
+Implementation notes (9 Jul, agent report):
+- Files changed: RemoteBookAutoApplyQueue.cs (EnqueueFront + LinkedList-based priority queue,
+ dedupe-preserving); TeamCollection.cs (QueueBookForBackgroundDownload/PrioritizeBackgroundDownload,
+ DownloadMissingBookInBackground, ProcessAutoApplyRemoteChange's new missing-folder branch,
+ SyncAtStartup's cloud rerouting); CloudJoinFlow.cs (blocking call removed, enqueue loop added);
+ CloudTeamCollection.cs (TryGetBookInstanceIdForName/TryGetBookNameForInstanceId/PrioritizeDownload);
+ CollectionApi.cs (BookListEntry DTO, ComputeNotYetDownloadedBookEntries pure merge function +
+ GetNotYetDownloadedBookEntries wiring via TeamCollectionApi.TheOneInstance -- no new constructor
+ dependency needed, following SharingApi's existing precedent -- and TryPrioritizeNotYetDownloadedBook
+ for the graceful selected-book handling); BookButton.tsx (placeholder render branch + new
+ CollectionTab.BookNotYetDownloaded tooltip string); BooksOfCollection.tsx (IBookInfo.notYetDownloaded).
+- New XLF string CollectionTab.BookNotYetDownloaded added to Bloom.xlf, translate="no", flagged as
+ provisional placement pending John's priority confirmation (note in the entry itself suggests
+ BloomMediumPriority.xlf as a likely alternative). The pre-existing sibling progress-message ids in
+ this same code path (JoiningCloudCollection, FetchedNewBook, and this task's new
+ FetchingNewBookInBackground) have NO XLF entries at all -- an established (if arguably
+ incomplete) precedent for TeamCollection sync-dialog progress text in this codebase; the new one
+ was left unlocalized to match, flagged here rather than silently deviating.
+- Tests: C# required filter 393/393 green (15 new: 4 EnqueueFront tests + 1 real-async EnqueueFront
+ sanity test in RemoteBookAutoApplyQueueTests.cs; 2 missing-folder ProcessAutoApplyRemoteChange
+ tests + 3 SyncAtStartup rerouting tests in TeamCollectionAutoApplyTests.cs, using
+ TestFolderTeamCollection's existing AutoApplyRemoteChangesForTests toggle so the shared
+ base-class logic is exercised without needing a full CloudTeamCollection; 6 new
+ CollectionApiTests.cs tests for the pure ComputeNotYetDownloadedBookEntries merge function).
+ CloudSyncAtStartupTests.SyncAtStartup_NewBookOnlyInRepo_IsFetchedToLocal updated per this item's
+ own instruction (TestOnly_MakeAutoApplyQueueSynchronous added, assertion kept, reasoning
+ documented in the test). BookButton.test.tsx (new, 5/5 green) covers the placeholder
+ render/label/click-priority-bump behavior and the unaffected normal-button paths. yarn typecheck
+ and eslint show no NEW errors/warnings introduced (compared before/after via git stash) beyond
+ this codebase's large pre-existing unrelated baseline of typecheck errors.
+- Deliberate omissions/risks for the orchestrator to re-verify live: (1) the status-panel
+ simplification noted above; (2) no dedicated CloudJoinFlow test file was added (no existing
+ FakeRestExecutor-based harness for it, and the diff there is a small, low-risk 3-line change
+ covered indirectly by the queue's own tests) -- the orchestrator's join-auto-open E2E run is the
+ real coverage for this path; (3) CollectionApi.HandleBooksRequest now calls
+ CloudTeamCollection.GetBookList()/EnsureCacheHydrated() on every collections/books request for a
+ cloud TC, which may trigger a network hydrate call the first time (idempotent afterwards) --
+ minor latency risk, not previously present in this endpoint; (4) the placeholder's "id" is the
+ book's stable InstanceId (matches BookInfo.Id once downloaded) so the React key doesn't change
+ across the download -- worth an E2E spot-check that the placeholder truly swaps in place rather
+ than flickering/remounting; (5) no dedicated E2E spec for the placeholder/priority behavior was
+ added (existing join-auto-open + e2e-9-new-book-lifecycle only) -- consider one if the live
+ verification surfaces gaps.
+
+### 8. Recovery safety net (John decision, 9 Jul — replaces the old "recovery preconditions"
+question) `[quick-medium]`
+Status: NOT STARTED
+John's spec: when a sync operation brings a remote version of a book to local but the local
+copy has somehow changed (rare — e.g. force-steal while edited, or any unexplained local
+drift), GO AHEAD and make local consistent with the TC, but FIRST save the previous local
+version as a .bloomSource so nothing is lost. SaveLocalCopyForRecovery (CloudTeamCollection,
+~line 668: zips to /Lost and Found/.bloomSource + logs an incident)
+already does exactly this and the STARTUP sync path already uses it (pinned by
+CloudSyncAtStartupTests.SyncAtStartup_LocalEditConflictsWithRemoteChange_...). The gap is
+the two RUNTIME overwrite paths, made urgent by item 4+5's auto-apply (whose eligibility
+gates use IsCheckedOutHereBy(GetLocalStatus) — dead-false for cloud, since cloud checkout
+never writes the local status file; see tasks/09-e2e.md E2E-4 finding):
+- [ ] In ProcessAutoApplyRemoteChange (TeamCollection.cs): before CopyBookFromRepoToLocal,
+ if the local folder's current checksum differs from the local status checksum (local
+ changed since last sync), preserve via a new virtual seam (base no-op; cloud override
+ → SaveLocalCopyForRecovery) — then apply as normal.
+- [ ] Same guard in TeamCollectionApi.HandleReceiveUpdates (the Sync button loop).
+- [ ] Unit tests through TestFolderTeamCollection (seam already has the synchronous-queue
+ test hooks); assert preserve-called-iff-locally-modified.
+- [ ] NOT needed (per John): persisting cloud checkout state to the local status file —
+ that was only required to reproduce folder-TC *blocking* semantics; John chose
+ apply-and-preserve instead.
+- [ ] E2E: this unblocks E2E-4's blocked .bloomSource sub-requirement — extend that spec
+ when convenient.
+
+### 9. Account-switch behavior (John decision, 9 Jul — unblocks E2E-10) `[medium-large]`
+Status: CODE DONE on branch `task/b1-9-account-switch` (created from origin/cloud-collections;
+not yet merged) — E2E verification QUEUED (orchestrator's job; hard rules forbade
+launching Bloom/e2e for this task)
+John's spec: local machine access is unrestricted; only shared-data operations are gated by
+the CURRENT logon's server permissions. Collection was joined under account A, Bloom now
+signed in as B:
+- [x] B NOT a member of the TC → REFUSE to open the collection. Message must name the current
+ logon, say it is not a member, give the admin email(s) to ask for membership, and name
+ the last team member who edited this collection on this machine.
+ TeamCollectionManager.CheckConnection gained an `allowHardRefusal` parameter (default
+ false, preserving every existing mid-session caller); only the constructor's initial
+ open-time call passes true. CloudTeamCollection.CheckConnection's non-member branch now
+ sets a new TeamCollectionMessage.IsAccessRefusal flag and composes the full detail text
+ (admins + last-known-user, see ComposeNotAMemberRefusalDetail) instead of the old
+ one-line message; a hard-refusal message throws the new
+ TeamCollectionAccessRefusedException, which propagates up through Autofac/ProjectContext
+ to Program.HandleErrorOpeningProjectWindow (new early special-case: plain message box,
+ no "Report this crash" flow, then falls through to the existing chooser-reopen path
+ exactly like any other failed project open).
+- [x] B IS a member → open CONNECTED. Books locally checked out by A show as checked out by A
+ but may be edited as if checked out by B — ONLY if the server state would have let A edit
+ here (i.e. A's lock is for THIS machine; not if A holds it elsewhere). On first edit of
+ such a book, atomically switch the checkout everywhere to B. If B checks it in (even
+ without editing), history records the checkin by B.
+ New virtual seams on TeamCollection (IsEditableHere, CanTakeOverLockOnThisMachine,
+ TryTakeOverLock) default to today's strict behavior for folder TCs; CloudTeamCollection
+ overrides them for the same-machine-different-account case. New RPC
+ tc.checkout_book_takeover (migration 20260709000007) atomically reassigns a lock from a
+ different account to the caller ONLY when the existing lock's machine matches — purely
+ additive, does NOT modify checkin_start_tx/checkin_finish_tx. The takeover call happens
+ in PutBookInRepo just before check-in (there is no per-keystroke "edit happened" hook
+ anywhere in this codebase — confirmed by research — so "on first edit" is implemented as
+ "on first check-in of that edit", the earliest point a takeover has any observable
+ effect) and in AttemptLock (for an explicit "check out" click, though the UI is unlikely
+ to show that affordance here since IsEditableHere already reports the book as usable).
+ Checkin attribution already falls out for free (checkin_finish_tx uses the caller's JWT).
+
+### 10. AWSSDK.S3 version bump (John decision, 9 Jul: take it on this branch) `[quick-medium]`
+Status: CODE DONE + SUITES GREEN on branch `task/b1-10-awssdk-bump` (bump 9b81c6040; not yet
+merged) — remaining: orchestrator's e2e-1 + e2e-2 through MinIO, then John's [HUMAN] web
+up/download check
+- [x] Bump AWSSDK.S3 (and its AWSSDK.Core pair) to current stable in the csproj(s); check
+ whether BloomHarvester/other projects pin the same package family and must move in
+ lockstep. DONE: BloomExe.csproj Core 3.5.1.32 -> 4.0.100.3, S3 3.5.3.10 -> 4.0.100.3
+ (major v4 jump); server/dev/parity-check floats 3.* -> 4.*. No other project in this
+ repo pins the family (BloomHarvester is a separate repo; there is no central
+ Directory.Packages.props — per-csproj pins are the convention). AWSSDK.SecurityToken
+ is not referenced anywhere (per-book session creds arrive as plain strings from the
+ edge functions), so only S3+Core move. project.assets.json confirms no SIL package
+ transitively pins AWSSDK.Core. v4 adjustments (details in commit 9b81c6040): checksum
+ config RequestChecksumCalculation/ResponseChecksumValidation=WHEN_REQUIRED on the two
+ MinIO-facing client builders (CloudBookTransfer.BuildDefaultClient,
+ CloudTeamCollection.BuildS3Client) because v4's WHEN_SUPPORTED default sends CRC32/
+ CRC64 trailing checksums S3-compatible endpoints may reject; BloomS3Client (real AWS
+ only) deliberately keeps the v4 defaults. Null-collection/bool? compile+runtime fixes
+ in S3Extensions.ListAllObjects and BloomS3ClientTests.DeleteFromUnitTestBucketAsync;
+ removed two orphaned usings that broke the v4 compile (ThirdParty.Json.LitJson was
+ embedded in AWSSDK.Core v3 and is gone in v4).
+- [x] Suites: cloud filter + ONE full BloomTests run (AWSSDK is used by the BloomLibrary
+ web-upload code — WebLibraryIntegration — so cloud-only filters are NOT sufficient).
+ DONE: baseline full run on UNMODIFIED cloud-collections FIRST (so pre-existing
+ failures can't be blamed on the bump): 3036 passed / 0 failed / 13 skipped / 3049
+ total. Post-bump: cloud filter 387/387; full run 3036 passed / 0 failed / 13 skipped
+ / 3049 total — identical to baseline, zero regressions. S3-specific fixtures called
+ out explicitly: CloudBookTransferTests 11/11, BloomS3ClientTests +
+ CloudEnvironmentTests' S3ForcePathStyle test, 44/44 in the combined ~S3/~
+ CloudBookTransfer/~BloomS3Client filter, including the LIVE
+ DownloadBook_DoesNotExist_Throws which hit the real BloomLibraryBooks-UnitTests
+ bucket with the v4 client (validating the null-S3Objects fix against real AWS).
+- [ ] E2E: at least e2e-1 + e2e-2 (S3 up/down through MinIO exercises the new SDK's
+ path-style + AssumeRole handling — the risky surface for a bump).
+- [ ] [HUMAN, John] Manual check that web book upload (publish to bloomlibrary.org) and
+ download (into Bloom) still work — recorded in GOING-LIVE.md 4.3.
+
+## Also queued from dogfooding (not in John's list, orchestrator-flagged)
+- Administrators field shows the REGISTRATION email (john_thomson@sil.org) instead of the
+ signed-in account email for cloud TCs (`ConnectToCloudCollection` sets
+ `Settings.Administrators = new[] { CurrentUser }`) — cosmetic identity-model
+ inconsistency, fix opportunistically with item 4+5 or 6.
+- Tier-timing fix (GOING-LIVE.md Phase 5, `task/b1-tier-timing`): `CheckDisablingTeamCollections`
+ gated solely on `CurrentCollection == null`, which for a cloud TC doesn't mean
+ "Settings.Subscription is trustworthy" (CurrentCollection is set before the connect-and-sync
+ sequence completes, and that sequence's success depends on cloud sign-in timing plus an S3
+ download that silently swallows exceptions) — so a healthy cloud TC could be permanently
+ disabled for the session on a stale/blank subscription snapshot. Fixed by deferring the cloud
+ check (WorkspaceModel) until after the collection-file sync, and re-reading the SubscriptionCode
+ fresh from disk at that point instead of trusting the in-memory snapshot. Folder-TC behavior/
+ timing unchanged. See branch for full diagnosis + tests.
+
+## OUTSTANDING BUGS (10 Jul 2026 PM — the current work list)
+0. **RESOLVED 11 Jul 2026 — implemented as option (a), per John's ruling (recorded verbatim in
+ the 11 Jul progress entry): editing/takeover of a checkout is only legitimate in the local
+ copy of the collection where the book is checked out.** Implementation: migration
+ 20260711000003 adds `tc.books.locked_seat` (client-computed hash of the local collection
+ folder path — the "seat"), recorded by checkout_book/checkout_book_takeover; takeover
+ requires machine AND seat match, and a NULL stored seat never matches (fail-safe); a
+ trigger clears the seat with every unlock path. Client: CloudTeamCollection.SeatId;
+ IsEditableHere/CanTakeOverLockOnThisMachine seat-gated (own pre-seat locks grandfathered;
+ other accounts strict). CONTRACTS.md bumped to v1.5. pgTAP 65/65, C# filter 428/428,
+ e2e-4 PASSES. FOLLOW-UP flagged for John (not blocking): checkin_start_tx still accepts a
+ same-user check-in regardless of seat/machine (pre-existing behavior; the client-side
+ editable gate is the enforcement point today) — decide whether the server should also
+ refuse cross-seat check-ins by the SAME user. Original problem statement follows for the
+ record.
+ **[Original — NEEDS JOHN] Item 9's same-machine takeover can steal ANY same-machine
+ lock, even across separate collection folders (found by e2e-4 after its download bugs were
+ fixed).** Scenario: Bob (admin) force-unlocks Alice's checkout and takes the lock himself;
+ Alice's later attemptLockOfCurrentBook RETURNS FALSE — but the server lock silently ends up
+ reassigned to ALICE, because item 9's takeover path (AttemptLock → TryTakeOverLock →
+ checkout_book_takeover) fires whenever the existing lock's MACHINE matches, and in E2E (and
+ any genuinely shared computer) every user is on the same machine. The machine-match gate
+ cannot distinguish John's intended scenario ("collection was joined under account A, B opens
+ the SAME local folder") from two users with SEPARATE local folders on one computer (two
+ 'seats', which is what e2e-4 simulates and what a shared lab machine would really be).
+ Design options sketched for John:
+ (a) Server-side 'seat': checkout_book/checkout_book_takeover store+compare a per-local-folder
+ id (e.g. hash of folder path) alongside machine — cleanest semantics, needs a migration +
+ pgTAP + client change;
+ (b) Client-side gate on the LOCAL folder's own state: only allow takeover if THIS folder
+ shows evidence the lock holder was using THIS folder. TeamCollectionLastKnownUser.txt
+ does NOT work for this as-is (CheckConnection overwrites it with the NEW user at open
+ time, before any takeover); writing a minimal local status record at cloud checkout would
+ work but John earlier decided cloud checkouts deliberately DON'T write local status;
+ (c) Accept the behavior (any same-machine user can take over any same-machine lock) and fix
+ e2e-4's expectation — probably wrong: it makes force-unlock semantics unreliable on
+ shared machines, and the takeover is SILENT (attemptLock even reported false while the
+ server lock changed hands — at minimum that inconsistency is a bug in any option).
+ Suggested: (a). Until decided, e2e-4 fails at its 'server lock is exactly Bob's' assertion
+ (spec line ~166). The e2e-4 DOWNLOAD failures that motivated the original bug #1 are FIXED
+ (see below).
+1. **e2e-4 background download fails + retry skipped (FIXED 10 Jul PM, verified by rerun —
+ the book now downloads in ~5s; kept for the record).** Evidence
+ (bob-joined SIL log, 14:51, preserved by the new durable logging): the one real download
+ attempt failed with `Could not find file 'C:\Users\\AppData\Local\Temp\
+ BloomCloudTCDownload\A5 Portrait.htm'` — the cloud download STAGING FOLDER IS A FIXED
+ SHARED TEMP PATH (`Temp\BloomCloudTCDownload`), so concurrent instances (two Blooms run in
+ every two-instance spec, plus any leftover files from earlier runs/specs) can clobber or
+ half-empty each other's staging area mid-copy. FIX: make the staging dir unique per
+ download (e.g. `BloomCloudTCDownload--`), clean up after. Secondly: after
+ that failure, Alice's checkout made QueueMissingRepoBooksForBackgroundDownload skip every
+ retry (books locked by ANYONE are skipped). The skip is TOO BROAD — a book locked by
+ someone ELSE is still safely downloadable (that is exactly what Receive does); the skip
+ only needs to cover books locked BY THE CURRENT USER (the local-rename-mid-checkin edge,
+ where the old repo name intentionally has no local folder). FIX: change the guard in
+ TeamCollection.QueueMissingRepoBooksForBackgroundDownload from "locked by anyone" to
+ "locked by me", and add a unit test mirroring QueueMissingRepoBooks_BookLockedInRepo_SkipsIt
+ but with a foreign lock expecting download. Then rerun e2e-4.
+2. **e2e-5 + e2e-8 singles: BOTH PASSED (10 Jul PM, post-merge tree)** — confirms their 10 Jul
+ AM matrix failures were transient infra as suspected. Current single-spec scoreboard on the
+ merged + defect-fixed tree: e2e-3 ✅, e2e-5 ✅, e2e-8 ✅, e2e-10 ✅; e2e-4 ❌ blocked solely
+ on bug #0 (its download failures are fixed; it now fails at the takeover-semantics
+ assertion, spec line ~166).
+3. **Full E2E matrix** not yet run on the post-defect-fix, master-merged state (was 8/14
+ before the fixes; 10/14 under heavy load 10 Jul PM). Run it after John decides bug #0 (or
+ accept one known e2e-4 failure). Standalone scoreboard as of 10 Jul late evening (after
+ the queue-arrival spec fixes): e2e-3 ✅, e2e-5 ✅, e2e-6 ✅, e2e-8 ✅, e2e-9 ✅ (3/3),
+ e2e-10 ✅; e2e-4 ❌ blocked solely on bug #0.
+4. Cosmetic (tracked): Administrators field shows registration email, not signed-in email
+ (see "Also queued from dogfooding").
+5. **Preflight (10 Jul PM, John's request):** light-review pass over the day's diff found 2
+ valid adjacent holes, BOTH FIXED + tested (72246c2975): per-account (not per-instance)
+ claim_memberships guard; machine-aware lock skip in the requeue pass. The GitHub half of
+ preflight (draft PR, Devin, Greptile/CodeRabbit, CI gauntlet) is BLOCKED: `gh` is not
+ authenticated in the agent session — John must run `gh auth login`, then re-run
+ `/preflight` to create the draft PR and run the bot gauntlet.
+6. **Full C# suite: RESOLVED AS FLAKE** — the first run's single failure (1/3131) did not
+ reproduce on the identification rerun (3120 passed / 0 failed / 3133 total, merged tree).
+
+## Progress log
+(orchestrator appends: date · what was just completed · EXACT next action)
+- 11 Jul 2026 (early AM — POST-SEAT-FIX FULL MATRIX: 13/14, and the 14th is exonerated) ·
+ Matrix on the seat-fixed tree (36 min): **e2e-4 PASSED IN THE MATRIX** (bug #0 verified
+ under full load); sole failure e2e-5, which passed standalone immediately after (3.0 min)
+ = load flake. Root cause found anyway and HARDENED: the spec killed Alice before her
+ initial share's asynchronous v1 commit was guaranteed done (nothing between
+ createCloudTeamCollection and the kill waits for the book row) — killing her mid-first-
+ Send leaves no book row ever. Fix: 90s poll for current_version_seq >= 1 BEFORE
+ alice.kill(). Every scenario has now passed on this exact tree; the tight-timeout flake
+ class is systematically addressed (all queue/commit polls at the 90s convention:
+ e2e-5/6/7/9) · Next: execute SQUASH-PLAN.md (preconditions met: bug #0 fixed+verified,
+ matrix verdict in) → cloud-tc-for-review branch + PR, close #8048 with pointer; then
+ optionally one more matrix as the gold stamp; John: [HUMAN] tests + OUTSTANDING BUGS #0
+ follow-up decision.
+- 11 Jul 2026 (early AM — BUG #0 FIXED AND VERIFIED; bot gauntlet fully closed) · John's
+ ruling (his words, from the in-session Q&A): "we should only be allowed to edit (either as
+ the original user checking the book out, or taking it over) if it is being worked on here,
+ in this copy of the collection… as long as the book is checked out here (this local copy)
+ and the logged-in user is a member, editing and take-over of the checkout should be
+ allowed. (A different user who has a different copy of the collection open, like our bob
+ and alice collections, definitely can't do this.)" — i.e. option (a) extended to the
+ "checked out here" determination. IMPLEMENTED (details in OUTSTANDING BUGS #0): server
+ seat column + gated takeover + auto-clear trigger (migration 20260711000003), client
+ SeatId + seat-gated IsEditableHere/CanTakeOverLockOnThisMachine (seams now take bookName),
+ CONTRACTS.md v1.5. VERDICTS: pgTAP 65/65 (10 new seat cases incl. e2e-4's
+ same-machine-different-seat refusal), C# filter 428/428 (6 new), **e2e-4 PASS** (first
+ time since the defect hunt began), **e2e-10 PASS** (same-seat takeover intact). Earlier
+ same night: full matrix 12/14 (37 min, desktop unlocked after John returned; sleep
+ timeouts disabled via powercfg — the mid-run locks were the 120-min AC idle-sleep timer);
+ the two failures were e2e-4 (now fixed) and e2e-7 (standalone 2/2 = load flake; its 20s
+ first-commit poll bumped to 90s). GREPTILE RE-REVIEW: "all three findings correctly
+ resolved. No new blocking issues." Gauntlet state: Greptile complete+clean, Devin
+ size-failed (terminal), CodeRabbit not installed, CI green · Next: (1) final full matrix
+ (expect 14/14 — first ever fully-green matrix if it holds), (2) execute SQUASH-PLAN.md →
+ cloud-tc-for-review PR, close #8048 with pointer, (3) John: follow-up decision in
+ OUTSTANDING BUGS #0 (server-side same-user cross-seat check-in) + [HUMAN] tests (item 3
+ centered dialog, item 10 web up/download, GOING-LIVE.md 4.3).
+- 10 Jul 2026 (night) · FULL MATRIX ATTEMPT INVALID — 14/14 failed because the Windows
+ desktop LOCKED sometime after the standalone runs (LogonUI confirmed running afterward;
+ every failure is at connectOverCdp / launch, the locked-session signature the E2E rules
+ warn about). NOT a code regression: e2e-3/6/9 had passed standalone within the previous
+ hour on the same tree. Orchestrator error to not repeat: re-check LogonUI immediately
+ BEFORE every launch, not just at session start. No leaked Bloom processes; stack healthy
+ (functions serve re-served itself cleanly after the pgTAP db reset). Greptile thread
+ replies posted (all 3 findings fixed in b93d0c9d82) · Next: rerun `yarn test` (full
+ matrix) as the FIRST action once the desktop is unlocked — expect 13/14 (e2e-4 = bug #0);
+ a desktop-unlock watcher is armed in the orchestrator session to catch the moment.
+- 10 Jul 2026 (late evening — runbook step 1 COMPLETE + Greptile findings fixed) ·
+ **e2e-3/6/9 ALL GREEN STANDALONE.** e2e-3 passed as-is (pure load flake). e2e-6 FAILED
+ standalone and was a REAL spec bug: since item 7 (progressive join), a book new to an
+ instance arrives via the background download queue AFTER pollNowViaReceiveUpdates
+ returns — the spec read Bob's file immediately (evidence: the book folder existed on
+ disk moments after the assertion failed). Fixed: v1-baseline read is now an expect.poll
+ (90s, the harness convention); the two 20s ceilings on queue-driven arrivals (e2e-6 v2
+ arrival, e2e-9 first test) bumped to 90s. e2e-9 then 3/3 — its one intermediate failure
+ (name-race alice: 0-byte stdout at launch) was load I caused myself by running
+ lint/vitest during the run; reran truly idle → green. LESSON REINFORCED: "standalone"
+ means the AGENT runs nothing else concurrently either. **GREPTILE (bypass) DELIVERED:
+ 1 P1 + 2 P2, all verified real and FIXED:** (P1/security) checkin-start scoped S3 write
+ creds to the CALLER-SUPPLIED bookInstanceId — checkin_start_tx never validates it for
+ existing books, so any member could get write creds for any book's prefix in their
+ collection; now reads the DB-canonical instance_id back (same selectTcRow pattern as
+ checkin-finish) + new deno test pinning that a mismatched client value cannot steer the
+ prefix. (P2) reap_expired_checkin_transactions returned only the collection-file count
+ (GET DIAGNOSTICS clobbered the loop total) → new migration 20260711000001. (P2)
+ checkout_book_takeover raised P0002/42501 bare strings instead of the schema-wide
+ PT404/PT403 JSON convention (C# would map both to CloudErrorCode.Unknown) → new
+ migration 20260711000002 (logic untouched); pgTAP 4a expectation updated. Also fixed
+ Greptile's style note: JoinCloudCollectionDialog.tsx nested ternaries → if/else chains
+ (12/12 vitest, lint+prettier clean). Suites: pgTAP 55/55 on the reset stack; deno
+ 33/33 (NOTE: invariants.test.ts needs --allow-read; without it 2 tests fail on file
+ access, not logic). CONTRACTS.md check: the takeover row was ALREADY added in v1.4 —
+ the "flagged, not applied" comments in 20260709000007/CloudTeamCollection.cs are stale
+ · Next: reply to + resolve the Greptile threads on PR #8048, push, then bug #0 (John),
+ squash plan, human tests.
+- 10 Jul 2026 (evening — RESUMED after machine sleep; runbook step 1 + bot gauntlet closure) ·
+ Environment: containers survived sleep (all healthy), functions serve restarted per the
+ zombie rule, smoke.ps1 3/3 PASS, desktop unlocked. e2e-3 STANDALONE: **PASS** (3.1 min,
+ idle machine) — its matrix failure confirmed as a load flake; e2e-6/e2e-9 standalone runs
+ in progress. BOT GAUNTLET now TERMINAL for PR #8048 (no more waiting): **Devin FAILED —
+ "This pull request's diff exceeds the size limit for analysis"** (its review page's Info
+ sidebar; no bypass exists, so Devin will also fail on the future squash-plan PR — same
+ 237-file diff); **Greptile REFUSED — 237 files > its 100-file limit** — but offers a
+ bypass, which was TAKEN: `@greptile-apps review` posted on #8048 (bot findings may arrive
+ async; check the PR's comments/reviews next visit); **CodeRabbit is NOT INSTALLED on this
+ repo** (zero comments ever, repo-wide search; no .coderabbit.yaml) — last session's
+ "timed out after 35 min" was waiting on a bot that isn't there; drop it from all future
+ waits in this repo; CI 2/2 pass (unchanged) · Next: e2e-6/e2e-9 standalone verdicts, then
+ the remaining runbook order (bug #0 = John, squash plan, human tests).
+- 10 Jul 2026 (EOD — SHUTDOWN STATE; machine going to sleep; next session may be a different
+ agent: read this entry + OUTSTANDING BUGS + SQUASH-PLAN.md and you have everything) ·
+ FULL MATRIX under HEAVY LOAD: 10/14 (40 min, ran concurrently with the 16-min full C#
+ suite + review agents — the known load-correlated-flake regime). Failures: e2e-4
+ (EXPECTED — bug #0, John's pending decision), e2e-3 / e2e-6 / e2e-9 (all three are
+ suspected LOAD FLAKES: e2e-3 passed standalone TWICE earlier today on this exact tree;
+ e2e-6/e2e-9 were green in the last pre-batch matrix; artifacts in
+ src/BloomTests/e2e/test-results/). BOT GAUNTLET at cutoff: CI 2/2 pass (pr-automation +
+ track; heavy CI doesn't run on this draft); CodeRabbit TIMED OUT after 35 min (no
+ review/comment via API); Devin TIMED OUT this session (huge PR — only the diff tree
+ renders on its page, no findings pass yet for HEAD; it keeps analyzing server-side).
+ Devin/CodeRabbit results will simply be waiting on PR #8048 whenever checked next.
+ SQUASH-PLAN.md committed (d8ff5c830e): review-grained packaging branch design, 9 grouped
+ commits, regenerable, byte-identical-verified · NEXT SESSION, in order: (1) rerun e2e-3,
+ e2e-6, e2e-9 STANDALONE on an idle machine (expect green; investigate for real if any
+ fails again), (2) John: bug #0 decision (options in OUTSTANDING BUGS #0; ready-to-implement
+ option-(a) sketch in BUG0-OPTION-A-SKETCH.md, same folder), implement + rerun e2e-4,
+ (3) gather bots: run the devin-review skill against PR 8048 (it mirrors findings to the
+ PR) + read CodeRabbit's review if posted; fix/reply per preflight rules, (4) execute
+ SQUASH-PLAN.md once 1–3 are done, open the new PR from cloud-tc-for-review, close 8048
+ with a pointer, (5) John's [HUMAN] tests: item 3 centered dialog, item 10 web
+ up/download (GOING-LIVE.md 4.3). Environment reminders for the resumer: functions-serve
+ zombie rule (server/dev/README.md) after any sleep/restart of the stack; E2E needs
+ BLOOM_E2E_SCREEN=1 and an unlocked desktop; front-end is pnpm now (e2e harness stays
+ yarn).
+- 10 Jul 2026 (PM, gauntlet running) · John authenticated gh. DRAFT PR CREATED:
+ https://github.com/BloomBooks/BloomDesktop/pull/8048 (cloud-collections → master, draft).
+ Devin triggered for HEAD 24b0f5c740 via the pr-automation workflow (completed = trigger
+ loaded); CodeRabbit + CI self-triggered on the PR. FULL E2E MATRIX running concurrently
+ (expected: 13/14, e2e-4's takeover assertion the only known failure — bug #0 pending
+ John's decision). If this session is cut off mid-gauntlet: re-run `/preflight` in a fresh
+ session — it re-enters wherever the PR/bots currently are (the devin-review skill gathers
+ + mirrors any finished Devin findings; matrix results land in the next entry) · Next:
+ poll bots (~30 min cap) → mirror Devin findings → fix/reply → matrix verdict → John's
+ decisions (bug #0, human tests).
+- 10 Jul 2026 (PM, preflight — END-OF-SESSION STATE) · Preflight (John's request) ran to the
+ limit of what the session could do: LOCAL HALF COMPLETE — light-review sub-agent over the
+ day's diff found 2 valid adjacent holes, both FIXED + unit-tested + pushed (72246c2975:
+ per-account claim_memberships guard — an in-session account switch would have resurrected
+ defect 2; machine-aware lock skip — your own other-machine checkout no longer blocks the
+ self-heal download). Gate results: cloud filter 422/422; FULL C# suite 3120/0/13 of 3133
+ (first run's single failure did NOT reproduce → flake); pnpm lint 0 errors; targeted vitest
+ 14/14; mergeability with origin/master clean (0 behind, 0 conflicts). E2E singles on the
+ final tree: e2e-3/5/8/10 ALL PASS; e2e-4 fails ONLY at the takeover-semantics assertion
+ (OUTSTANDING BUGS #0, John's decision — options a/b/c documented there, recommend (a)
+ server-side seat). GITHUB HALF BLOCKED: `gh` unauthenticated in the agent session, so no
+ draft PR / Devin / CodeRabbit / CI ran — after `gh auth login`, re-run `/preflight`.
+ Preflight report artifact (decisions + copy-back form) published for John · NEXT, in
+ order: (1) John: gh auth login + answer the report's decisions (esp. bug #0), (2) implement
+ bug #0 as decided + rerun e2e-4, (3) full E2E matrix, (4) re-run /preflight for the bot
+ gauntlet, (5) John's [HUMAN] tests: item 3 centered dialog, item 10 web up/download
+ (GOING-LIVE.md 4.3).
+- 10 Jul 2026 (PM, master integration) · cloud-collections is now UP TO DATE with
+ origin/master (c41fcfd2bd) — as a MERGE, not the planned rebase, deliberately: a true
+ rebase meant replaying 189 commits over a master that already contains cherry-picked
+ batch commits (e.g. the Common.BloomIsBusy l10n fix is master's tip), and it started
+ conflicting at commit 4/189 (add/add on files master partially has); cloud-collections
+ also already has merge-style history (task merges), so linearizing + force-pushing a
+ shared branch was worse than integrating. The merge itself completed with ZERO conflicts
+ (the feared overlap files — 2 XLF, CollectionApi.cs, ExternalApi.cs — all auto-merged;
+ the cherry-picked l10n fix was byte-identical on both sides). A safety branch
+ `cloud-collections-pre-rebase-2026-07-10` marks the pre-merge state. PNPM: the front-end
+ (src/BloomBrowserUI, BloomVisualRegressionTests, src/content) is now pnpm 11.5.2 — NEVER
+ yarn/npm there anymore (root AGENTS.md updated by master); the E2E harness
+ (src/BloomTests/e2e) deliberately KEEPS its own yarn.lock (unaffected by the migration)
+ · Next: pnpm install + C# cloud filter on the merged tree (running), push
+ cloud-collections, then OUTSTANDING BUGS #1 (e2e-4) and the remaining test pipeline.
+- 10 Jul 2026 (PM, later) · e2e-4 rerun FAILED with a NEW, fully-diagnosed signature (see
+ OUTSTANDING BUGS #1 above — fixed shared download staging folder + over-broad locked-book
+ skip in the new retry pass). Per John's live instruction: pausing the test loop here,
+ merging the defect-fix branch, then REBASING cloud-collections onto current origin/master
+ (~62 commits incl. the pnpm migration) before returning to test fixes · Next: merge
+ task/b1-postbatch-defects (fast-forward) + push, rebase, post-rebase build sanity, then
+ fix OUTSTANDING BUGS #1 and rerun e2e-4/5/8 + full matrix, then John's [HUMAN] checks.
+- 10 Jul 2026 (PM — HANDOFF ENTRY; possibly the last session with this agent for a while;
+ written for human/agent resumers weeks later) · ALL THREE post-batch defects DIAGNOSED,
+ FIXED, COMMITTED on `task/b1-postbatch-defects`, unit suites green (cloud filter 418/418),
+ and E2E-verified per the fail-fast protocol (one failing spec at a time, no full-suite
+ reruns until each passed). Root causes, for the record:
+ · DEFECT 1 (books never arrived after join-relaunch; e2e-3/e2e-4): the pullDown→kill→
+ relaunch pattern guarantees the in-memory RemoteBookAutoApplyQueue dies with the process;
+ the relaunch's SyncAtStartup rerouting was the only redelivery path and every failure in
+ that pipeline was SILENT, while the poll only raises events for books whose repo state
+ CHANGED — so one miss = book missing forever. Fix (4339e02d60): new
+ TeamCollection.QueueMissingRepoBooksForBackgroundDownload (queues every unlocked repo
+ book with no local folder), called from CloudTeamCollection.StartMonitoring (post-sync)
+ and after every OnPolledChanges — any drop now self-heals within one poll interval; plus
+ durable SIL logging on all previously-silent paths (incl. ReportProgressAndLog, whose
+ startup-sync record previously vanished with the collection folder). 4 new unit tests.
+ VERIFIED: e2e-3 PASSED (was the failing waitForBookFile signature).
+ · DEFECT 2 (e2e-10 bob-takeover: alive 90s, empty stdout, no window/server): dotnet-stack
+ dump of the live hung process showed the UI thread blocked in MessageBox.Show inside
+ TeamCollectionManager's ctor. Chain: an APPROVED-but-never-CLAIMED membership (bob opens
+ ALICE's local folder — item 9's shared-computer scenario — so bob never ran the join flow,
+ the only place claim_memberships was called) passes CheckConnection's EMAIL-based
+ my_collections check, then get_collection_state throws not_a_member (RLS gates are
+ user_id-based) during the ctor's first sync; the generic catch shows a MODAL MessageBox
+ no automation can dismiss. Fix (ae35b87c34): CheckConnection now calls ClaimMemberships
+ (idempotent, once per session) on membership confirm; NonFatalProblem.Report in
+ --automation mode writes BLOOM_AUTOMATION_NONFATAL_PROBLEM + stack to stdout and returns
+ instead of blocking (mirrors the RunningInConsoleMode guard) — any future startup report
+ is a readable harness-log line, never a silent hang. New unit test pins the claim call.
+ VERIFIED: e2e-10 PASSED end-to-end (refusal + takeover-checkin attribution).
+ · DEFECT 3 (Cannot Find API Endpoint teamCollection/capabilities toast): the endpoint was
+ project-level but is legitimately probed with no project open — the E2E harness readiness
+ poll (proven: a probe landed BEFORE any WebView2 existed in bob's 10:11 log) and late
+ calls from a closing collection tab while the chooser is up (John's sighting; the item-6
+ "chooser bundle hook" hypothesis was DISPROVEN — no chooser component calls it). Fix
+ (4819eda881): registration moved to the app-level SharingApi (TheOneInstance precedent),
+ all-false when no project/TC is current. Fallout fix (c24af86042): two harness call
+ sites that single-shot-asserted supportsSharingUi===true right after a relaunch now use
+ the same 20s expect.poll as every other site (the app-level endpoint answers
+ truthfully-false while the project is still opening; the old one-shots only ever passed
+ because a project-level registration race hid the timing). VERIFIED by the e2e-3/e2e-10
+ passes above (both exercise the polled path).
+ Also noteworthy: the window-placement watcher (7029006d5) is CONFIRMED working now
+ (windowPlacement.log files written, windows moved to the spare screen); the missing SIL
+ Log-tmp files for hard-killed instances are expected (SIL Logger doesn't flush on kill) —
+ stdout via the NonFatalProblem automation line is now the reliable channel · NEXT ACTIONS,
+ in order: (1) e2e-4 + e2e-5 + e2e-8 singles (running/queued at handoff time — see the next
+ entry if one was added, else run them first), (2) merge `task/b1-postbatch-defects` into
+ `cloud-collections` (fast-forward; branch is strictly ahead) + push, (3) FULL E2E MATRIX
+ (cd src/BloomTests/e2e && yarn test; ~30 min, desktop unlocked, stack up — remember the
+ functions-serve zombie rule in server/dev/README.md), (4) rebase cloud-collections onto
+ origin/master — now ~62 commits behind incl. the pnpm migration; only ~4 overlapping files
+ expected (2 XLF, CollectionApi.cs, ExternalApi.cs); after rebasing, remember the front-end
+ package manager may switch from yarn to pnpm on the rebased branch — re-read the rebased
+ AGENTS.md before running front-end commands, (5) post-rebase full matrix, (6) John's
+ [HUMAN] checks: item 3 centered-dialog visual, item 10 web upload/download
+ (GOING-LIVE.md 4.3), John's dogfood-plan decision. Open cosmetic item: Administrators
+ shows registration email (see "Also queued from dogfooding").
+- 10 Jul 2026 (resumed again after VS Code restart) · Verified state: main tree clean on
+ `task/b1-postbatch-defects` at b0941db62, no worktree WIP. Dev stack was BROKEN at resume:
+ edge-runtime container missing entirely and no `supabase functions serve` process (several
+ supabase containers had restarted ~45 min prior) — restarted functions serve with
+ server/dev/functions.env, endpoint now answering (401 on bare probe = healthy), edge
+ container up. Relaunched the three-defect diagnosis/fix agent (defects 1–3 from the 10 Jul
+ AM entry) on the existing branch in the main tree · Next: review + merge that branch, then
+ rerun e2e-3/4/5/8/10, then full matrix → rebase onto origin/master → post-rebase matrix →
+ John's visual checks.
+- 10 Jul 2026 (resumed after VS Code restart) · Verified resume state: working tree clean
+ at ff6c5a6f8, no uncommitted worktree work, dev stack healthy (edge-runtime container
+ restarted ~15 min prior but has its BLOOM_* env — NOT a functions-serve zombie).
+ Relaunched the three-defect diagnosis/fix agent (defects 1–3 from the 10 Jul AM pause
+ note) on branch `task/b1-postbatch-defects` in the main tree · Next: review + merge that
+ branch, then rerun e2e-3/4/5/8/10, then full matrix → rebase onto origin/master →
+ post-rebase matrix → John's visual checks.
+- 10 Jul 2026 (PAUSED again for another restart, John's request) · The three-defect agent
+ was stopped while still in its read-only diagnosis phase — NO code work or commits lost
+ (branch `task/b1-postbatch-defects` contains only the two orchestrator log commits; main
+ tree is checked out on it, clean). The three defect descriptions in the 10 Jul AM entry
+ remain the full open work list; stack was verified healthy at resume time · Next action:
+ relaunch the three-defect diagnosis/fix agent on the existing
+ `task/b1-postbatch-defects` branch (defect descriptions above are self-sufficient), then
+ the unchanged pipeline: review/merge → e2e-3/4/5/8/10 → full matrix → rebase onto
+ origin/master → post-rebase matrix → John's visual checks.
+- 10 Jul 2026 (AM, PAUSED for VS Code restart) · State: all batch items 1–10 + tier-timing
+ fix MERGED and pushed (through commit 7029006d5). Post-batch E2E stabilization in
+ progress — full matrix run 1 was 8/14. Fixed + pushed since: AWSSDK-v4 null S3Objects
+ second site (DownloadCollectionFileGroup); item-9 sidecar idle-loop that starved the UI
+ thread (checkin timeouts; watcher-file feedback loop — see commit f451aa865); harness
+ waitForBookFile for progressive-join; e2e-10 refusal line to stdout
+ (BLOOM_AUTOMATION_REFUSED_COLLECTION); window watcher NEVER RAN (node detached spawn
+ kills powershell instantly — fixed non-detached + DPI-aware + spawn-time + placement
+ logs, commits 80b333c4c/7029006d5). THREE OPEN DEFECTS, diagnosis agent was killed
+ before starting (no work lost):
+ (1) background book download silently dropped after join-relaunch — hypothesis:
+ DownloadMissingBookInBackground's IsBookPresentInRepo pre-check on an unhydrated cache
+ returns false → silent return → dedupe means never re-queued (e2e-3/e2e-4 failures; no
+ RemoteBookAutoApplyQueue error lines in SIL logs = silent drop confirmed);
+ (2) e2e-10 'bob-takeover' relaunch never reaches BLOOM_AUTOMATION_READY (empty stdout;
+ check SIL Log-tmp*.txt ~10:0x AM Jul 10);
+ (3) collection chooser triggers 'Cannot Find API Endpoint teamCollection/capabilities'
+ toast (project-level endpoint called at app level; John saw it on screen; suspect a
+ hook item 6 pulled into the chooser bundle).
+ ALSO PENDING: full matrix re-run → rebase onto origin/master (47 commits incl. pnpm
+ migration; only 4 overlapping files: 2 XLF, CollectionApi.cs, ExternalApi.cs) →
+ post-rebase matrix → John's visual checks. e2e-5/e2e-8 retest failures were TRANSIENT
+ infra (podman/db-reset under load; verified clean after). Stack is up; remember the
+ functions-serve zombie rule (server/dev/README.md) after any supabase stop/start ·
+ Next action: relaunch the three-defect diagnosis/fix agent (its full brief is in the
+ orchestrator conversation; the three defect descriptions above are self-sufficient),
+ then rerun e2e-3/4/5/8/10, then the full pipeline above.
+- 9 Jul 2026 · Batch plan created; full-matrix baseline run in progress (validates
+ checkin-comment fix + 5s poll live) · Next: item 1 ("Bloom is busy" l10n) code work
+ while the matrix runs.
+- 9 Jul 2026 (PM) · 4-spec verification queue GREEN 4/4 in 9.5 min (e2e-1, join-auto-open,
+ e2e-2, e2e-8) on the merged state incl. items 1–6 and 8 — items 1/2 fully DONE, 4+5 E2E
+ verified, 6 join-flow regression clear. John decisions recorded: safety window 7d;
+ subscription tier same as folder TCs; AWSSDK bump on this branch (item 10) with [HUMAN]
+ web up/download check; account-switch spec (item 9); recovery spec (item 8, implemented,
+ 382/382). Remaining: item 7 (agent next), items 9/10, John's dogfood-plan decision +
+ visual dialog check · Next: launch item 7 implementation agent.
+- 9 Jul 2026 (later) · Baseline matrix 13/13 GREEN (31 min). Items 1–3 code done +
+ committed (2d74d280f, 6f0c4a068, 207cc1d0); unit suites green (C# 363/363, panel vitest
+ 11/11). Screen NOW LOCKED (John away): all Bloom-launching verification queued — e2e-1
+ (item 1 gate), e2e-2 (items 2+3), plus item 3 visual check. A Debug Bloom (PID 48012,
+ origin unknown, possibly John's) is running and locks output/Debug — build/test with
+ `-c Release` until it's gone; do NOT kill it without John · Next: item 4+5 design read
+ (CloudTeamCollection change-application path), code-only work.
+- 9 Jul 2026 (later still) · Item 4+5 CODE DONE on branch `task/b1-45-auto-sync` (created
+ from cloud-collections; not yet merged): TeamCollection.CanAutoApplyRemoteChanges +
+ RemoteBookAutoApplyQueue (auto-apply for cloud TCs, folder TCs unchanged); "Receive
+ Updates" renamed to "Sync" everywhere (dialog + per-book panel) plus a fixed
+ needsReload/updatesAvailable priority bug for cloud found along the way; XLF renamed
+ TeamCollection.ReceiveUpdates → TeamCollection.Sync. Diagnosis of the missing
+ "updates available" badge: it's driven solely by the message log's NewStuff milestone
+ (TeamCollectionStatus.NewStuff → teamCollection/tcStatus), a SEPARATE signal from
+ tcStatusMetadata's updatesAvailableCount (which was likely already correct/live) — see
+ the item's own diagnosis bullet above for detail. Tests: C# required filter 375/375
+ green (incl. new RemoteBookAutoApplyQueueTests + TeamCollectionAutoApplyTests); both
+ touched vitest files green (5/5, 13/13); yarn typecheck/eslint clean. E2E NOT run
+ (desktop locked; forbidden by this task's rules) — `e2e-2-collaboration-loop` +
+ `e2e-8-receive-during-send` + e2e-1 (XLF gate) queued alongside items 1–3's pending
+ runs · Next: orchestrator review + merge of task/b1-45-auto-sync into cloud-collections,
+ then the queued full E2E pass covering items 1–5, then item 6 (join-card integration).
+- 9 Jul 2026 (even later) · Item 6 CODE DONE on branch `task/b1-6-join-cards` (created from
+ cloud-collections; not yet merged): removed MyCloudCollectionsSection.tsx (+ test);
+ CollectionChooserApi gains `collections/getJoinCards` (SharingApi.GetMyCollectionsForJoinCards
+ for the signed-in check + cloud list, no network call when signed out; ComputeJoinCards is the
+ pure id-matching helper, internal static, unit-tested in new CollectionChooserApiTests.cs;
+ GetLocalCloudCollectionIds scans MRU + discovered local folders' TeamCollectionLink.txt files
+ the same way CloudJoinFlow.DetermineScenario does, but across ALL known folders rather than one
+ expected name, since a join card is about "has ANY local copy", not "would this name collide").
+ CollectionCard grows an isJoinCard variant (title + TC icon + reused "Get" cue only, no per-card
+ fetch, no Show-in-Explorer menu); CollectionCardList appends joinCollections AFTER its
+ maxCardCount(10) slice so they're never capped. CollectionChooser.test.tsx rewritten for the
+ card-based flow; new CollectionCardList.test.tsx covers the append-after-slice logic; new
+ CollectionCardList.stories.tsx "WithJoinCards" story. Removed 4 now-orphaned untranslated XLF
+ entries from the deleted sidebar (kept + repurposed CollectionChooser.PullDown, "Get", as the
+ join cue). C# required filter 380/380 green; CollectionCardList.test.tsx 4/4 and
+ CollectionChooser.test.tsx 3/3 green; yarn typecheck and eslint (changed files) clean. E2E NOT
+ run (desktop locked; forbidden by this task's rules) — `join-auto-open` (checked: drives
+ pullDown/openCollection directly via HTTP, doesn't touch the chooser UI, should be unaffected)
+ + `e2e-1-create-share` (XLF gate) queued · Next: orchestrator review + merge of
+ task/b1-6-join-cards into cloud-collections, then item 7 (progressive join) once the queued
+ E2E pass covering items 1–6 runs.
+- 9 Jul 2026 (agent) · Item 7 (progressive join) CODE DONE on branch
+ `task/b1-7-progressive-join` (created from origin/cloud-collections; not yet merged):
+ CloudJoinFlow no longer blocks on CopyAllBooksFromRepoToLocalFolder -- every repo book is
+ queued via the new TeamCollection.QueueBookForBackgroundDownload right after settings download,
+ so the collection opens immediately. CollectionApi.HandleBooksRequest merges the cloud repo book
+ list into the collections/books JSON (new BookListEntry DTO + pure
+ ComputeNotYetDownloadedBookEntries, unit-tested) so repo-only books show `notYetDownloaded:
+ true`; BookButton.tsx renders those as a simple placeholder (dashed border, cloud-download icon,
+ title, no thumbnail request, no context menu -- SAFETY: no dangerous action reachable).
+ RemoteBookAutoApplyQueue gained EnqueueFront (priority, never interrupts an in-flight download);
+ selecting a placeholder (CollectionApi's selected-book handler) gracefully bumps its download to
+ the queue front instead of crashing on the missing BookInfo. Each background download
+ (TeamCollection.DownloadMissingBookInBackground, the queue worker's new no-local-folder branch)
+ invalidates the cached book list and re-sends the existing editableCollectionList/reload
+ websocket event so the placeholder swaps for the real button automatically. SyncAtStartup's
+ "brand new book!" branch now reroutes to the same background queue for cloud
+ (CanAutoApplyRemoteChanges) instead of fetching synchronously, so a half-joined collection's
+ next open stays fast (folder TCs completely unaffected, pinned by a new regression test).
+ DEVIATION flagged for John/orchestrator: the "downloading" status indicator is a persistent
+ placeholder icon on the book button itself, not routed through the real BookSelection/preview
+ pane and TeamCollectionBookStatusPanel.tsx's StatusPanelState union as the scout notes'
+ exact seam suggested -- judged too risky (no real Book/local folder exists yet to fake a
+ selection with) for the value added; the functionally important part (priority bump) IS
+ implemented and tested. New XLF string CollectionTab.BookNotYetDownloaded added to Bloom.xlf,
+ provisional/translate="no", flagged for John's priority-file confirmation. Tests: C# required
+ filter 393/393 green (15 new across RemoteBookAutoApplyQueueTests, TeamCollectionAutoApplyTests,
+ and new CollectionApiTests.cs); CloudSyncAtStartupTests.SyncAtStartup_NewBookOnlyInRepo_IsFetchedToLocal
+ updated per this item's own instruction (queue now made synchronous for the test, assertion
+ unchanged, reasoning documented inline); new BookButton.test.tsx 5/5 green. yarn typecheck/eslint
+ show no NEW issues (verified via git-stash before/after diff against this codebase's large
+ pre-existing unrelated typecheck-error baseline). E2E NOT run (this task's hard rules forbid
+ launching Bloom/e2e) — `join-auto-open` + `e2e-9-new-book-lifecycle` queued for the orchestrator
+ · Next: orchestrator review + merge of task/b1-7-progressive-join into cloud-collections, then
+ the queued E2E pass, then items 9/10.
+- 9 Jul 2026 (agent) · Item 10 (AWSSDK bump) CODE DONE + SUITES GREEN on branch
+ `task/b1-10-awssdk-bump` (created from origin/cloud-collections; not yet merged): AWSSDK.Core
+ 3.5.1.32 -> 4.0.100.3 and AWSSDK.S3 3.5.3.10 -> 4.0.100.3 in BloomExe.csproj (major v4);
+ parity-check tool floats 3.* -> 4.*; no other project pins the family, AWSSDK.SecurityToken is
+ not referenced anywhere, no transitive SIL pin conflicts. v4 adjustments:
+ RequestChecksumCalculation/ResponseChecksumValidation=WHEN_REQUIRED on the two MinIO-facing
+ client builders (CloudBookTransfer, CloudTeamCollection) — v4's WHEN_SUPPORTED default sends
+ CRC32/CRC64 trailing checksums S3-compatible endpoints may reject; BloomS3Client (real AWS)
+ keeps v4 defaults. Null-collection/bool? fixes in S3Extensions.ListAllObjects +
+ BloomS3ClientTests; removed 2 orphaned usings (LitJson embedded in v3 Core, gone in v4).
+ Baseline full BloomTests on UNMODIFIED cloud-collections FIRST: 3036/0/13 (3049 total);
+ post-bump: cloud filter 387/387, full suite 3036/0/13 — identical, zero regressions;
+ S3-specific fixtures 44/44 incl. the LIVE DownloadBook_DoesNotExist_Throws against real AWS.
+ E2E NOT run (orchestrator's job): e2e-1 + e2e-2 through MinIO queued — watch for checksum
+ (should be silent now), path-style, and AuthenticationRegion behavior; then John's [HUMAN]
+ web up/download check (GOING-LIVE.md 4.3) · Next: orchestrator review + merge of
+ task/b1-10-awssdk-bump, then the queued E2E pass.
+- 9 Jul 2026 (agent) · Item 9 (account-switch behavior) CODE DONE on branch
+ `task/b1-9-account-switch` (created from origin/cloud-collections; not yet merged): refusal
+ path — TeamCollectionManager.CheckConnection(allowHardRefusal) (default false, only the
+ constructor's initial open-time call passes true) throws the new
+ TeamCollectionAccessRefusedException when CloudTeamCollection.CheckConnection's non-member
+ branch sets the new TeamCollectionMessage.IsAccessRefusal flag; Program.HandleErrorOpeningProjectWindow
+ special-cases that exception (plain message box, no crash-report flow) before falling through
+ to the existing chooser-reopen path. The refusal message composes admin email(s) (read from
+ the local .bloomCollection's Administrators field — flagged risk: this inherits the
+ already-tracked "Administrators shows registration email not signed-in email" bug from the
+ "Also queued from dogfooding" list, since that fix was out of this item's scope) and "last
+ known team member on this machine" from a NEW durable local record,
+ TeamCollectionLastKnownUser.txt (sidecar file next to TeamCollectionLink.txt; chosen over
+ extending TeamCollectionLink.txt's tightly-scoped tested format; written at join time
+ (CloudJoinFlow) and refreshed on every successful membership confirmation
+ (CloudTeamCollection.CheckConnection), so it doubles as "who joined" and "last confirmed
+ local user" — documented as an approximation, not literally "last edited"). Takeover path —
+ new virtual seams on TeamCollection (IsEditableHere/CanTakeOverLockOnThisMachine/
+ TryTakeOverLock, all no-op/strict by default so folder TCs are unaffected) let
+ CloudTeamCollection treat a book locked to a DIFFERENT account on THIS machine as editable
+ and checkin-able; new additive RPC tc.checkout_book_takeover (migration
+ 20260709000007_tc_checkout_takeover.sql) atomically reassigns the lock, called from
+ PutBookInRepo just before check-in (no per-keystroke "edit happened" hook exists anywhere in
+ this codebase, confirmed by research, so "on first edit" == "on first check-in of that edit")
+ and from AttemptLock (explicit checkout click, likely unreachable in the UI here but kept for
+ symmetry). checkin_start_tx/checkin_finish_tx are UNTOUCHED — purely additive, so no existing
+ RPC's contract changed. CONTRACTS.md addition flagged, NOT applied (orchestrator decision per
+ this task's rules): a `checkout_book_takeover(book_id, machine) -> {success, locked_by,
+ locked_by_machine, locked_at}` row alongside checkout_book/unlock_book/force_unlock. Tests: 55
+ pgTAP (42 existing + 13 new in 02_tc_checkout_takeover_test.sql, actually run against the
+ local dev stack — same-machine takeover, cross-machine rejection, no-op re-takeover,
+ non-member rejection all green); C# required filter (Cloud|TeamCollection|SharingApi) 406/406
+ green (17 new: 5 ComposeNotAMemberRefusalDetail + 2 CheckConnection refusal/last-known-user in
+ CloudTeamCollectionMemberTests.cs, 9 in new CloudAccountSwitchTakeoverTests.cs, 3 in new
+ TeamCollectionAccountSwitchRefusalTests.cs). One new XLF string,
+ TeamCollection.Cloud.NotAMemberRefusal, added to Bloom.xlf (translate="no"), FLAGGED
+ PROVISIONAL for John's priority-file confirmation — it's shown in a plain MessageBox, arguably
+ more user-facing than most existing unlocalized TC internal strings, so may deserve a
+ different priority file or eventual real translation. New (non-run) E2E spec
+ `e2e-10-account-switch.spec.ts` written for the orchestrator's next pass, replacing the old
+ blocked task-09 scenario of the same number (different shape now — open-time refuse/takeover,
+ not in-session block-with-choices); flags that the refusal MessageBox is a native Win32 dialog
+ invisible to CDP entirely, so the spec verifies it via the instance's own log file instead.
+ Known omissions/risks for the orchestrator: (1) the Administrators-email identity bug noted
+ above; (2) no automated test exercises PutBookInRepo's pre-checkin takeover call end-to-end
+ (would need a full book-folder + checkin-start/finish edge-function mock harness) — covered
+ indirectly by direct unit tests of the virtual seams plus the new E2E spec; (3) TestFolderTeamCollection's
+ own takeover behavior was not separately tested since CanTakeOverLockOnThisMachine's folder
+ default is `false` (unchanged behavior, no new folder-TC surface to test) · Next: orchestrator
+ review + merge of task/b1-9-account-switch, then the queued E2E pass including e2e-10.
+- 10 Jul 2026 (agent) · Tier-timing fix ("Also queued from dogfooding") CODE + TESTS DONE on
+ branch `task/b1-tier-timing` (created from origin/cloud-collections; not yet merged): diagnosis
+ — `TeamCollectionManager.CheckDisablingTeamCollections` (TeamCollectionManager.cs ~782) gates
+ solely on `CurrentCollection == null`; for a cloud TC that's set (TeamCollectionManager.cs ~364)
+ BEFORE the connect-and-sync sequence (~374-391) that is the only thing able to deliver a fresh,
+ repo-authoritative SubscriptionCode into `Settings.Subscription` — an in-memory CollectionSettings
+ snapshot captured once at ProjectContext startup and never reloaded mid-session. That sequence's
+ success depends on cloud sign-in readiness (`CloudTeamCollection.CheckConnection` short-circuits
+ on `!_auth.IsSignedIn`) and an S3 download that silently swallows exceptions
+ (`CloudTeamCollection.DownloadCollectionFileGroup`'s catch-and-report-only handler) rather than
+ propagating failure — so a cloud TC's subscription snapshot can still be stale/blank when the
+ check runs, permanently disconnecting a healthy collection for the session (matches the E2E-9
+ harness's observed ~1-in-40 misfire, tasks/09-e2e.md). Fix: `WorkspaceModel.HandleTeamStuffBeforeGetBookCollections`
+ now defers the check for cloud TCs to run inside `SynchronizeRepoAndLocal`'s `whenDone` callback
+ (after sync), and `TeamCollectionManager.GetSubscriptionForDisablingCheck` (new) re-reads the
+ SubscriptionCode fresh from the on-disk `.bloomCollection` file for a cloud TC instead of
+ trusting the in-memory snapshot; folder TCs (and the no-TC case) keep the original immediate
+ check, byte-identical. `TeamCollectionManager.CheckDisablingTeamCollections` and
+ `TeamCollection.SynchronizeRepoAndLocal` marked `virtual` (previously plain `public void`) purely
+ so test subclasses can observe call order/behavior without invoking a real progress dialog. New
+ tests: `TeamCollectionTierTimingTests` (misfire no longer disables; genuinely insufficient tier
+ still disables for cloud via fresh disk read; non-cloud path unaffected, in both directions) and
+ `WorkspaceModelTierTimingOrderingTests` (folder TC still checks-then-syncs; cloud TC now
+ syncs-then-checks) — 7 new tests, all green. Full required filter
+ `(~Cloud|~TeamCollection|~SharingApi)&!~LiveTests`: 413/413 (406 baseline + 7 new), zero
+ regressions. Risk for the orchestrator's E2E pass: the harness's `createScratchCollection`
+ (collectionFixture.ts) stamps a fake valid subscription code onto every scratch collection as a
+ workaround for this exact bug — with the fix merged, that workaround is likely safe to REMOVE
+ (or at least no longer load-bearing), but flagged for the orchestrator to verify live before
+ touching the harness, since removing it now means every E2E cloud-TC scenario exercises the real
+ timing path for the first time · Next: orchestrator review + merge of task/b1-tier-timing.
diff --git a/Design/CloudTeamCollections/orchestration/RESUME.md b/Design/CloudTeamCollections/orchestration/RESUME.md
new file mode 100644
index 000000000000..b21ee7b61c1f
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/RESUME.md
@@ -0,0 +1,65 @@
+# Cloud TC — agent orchestration & resume protocol
+
+> **In-flight batch (9 Jul 2026):** John's dogfood bug/improvement list is being worked
+> per [DOGFOOD-BATCH-1.md](DOGFOOD-BATCH-1.md) — resume THAT file's checklist first.
+
+This folder holds the launch prompts for in-flight implementation tasks and the protocol
+that makes them resumable across work sessions (including AI-session token limits).
+
+## The durable-state rule
+
+All task state lives in **git branches**, never in a conversation:
+
+- One branch per task, named `task/-`, based on `cloud-collections`. The
+ currently in-flight set = whatever `git branch --list "task/*"` shows unmerged into
+ `cloud-collections`. As of 8 Jul 2026 (evening) ALL Wave-4 code is merged and pushed:
+ harness, scenarios E2E-1..9 (E2E-4 partial, E2E-10 blocked — both are product decisions,
+ see tasks/09-e2e.md findings + GOING-LIVE.md Phase 5), task 10's 7 polish items, the
+ go-live/test-setup docs, and 3 product fixes from the scenario work. NO agent work is in
+ flight. What remains for Wave 4: (a) one 12/12 acceptance run of the full E2E matrix on
+ an IDLE machine (`cd src/BloomTests/e2e && yarn test`, desktop unlocked, ~30 min — five
+ runs on the busy dev machine each passed a different 8–11/12, all failures
+ load-correlated, worst offender = finding 9's problem-report modal); (b) John's product
+ decisions; (c) dogfood. All Wave-0/1/2/3 branches are merged; see IMPLEMENTATION.md.
+- Agents commit after EVERY completed checklist step — small, coherent commits; never one
+ big commit at the end. Tick the step's checkbox in the task file in the same commit.
+- Each task file ends with a `## Progress log` section; every commit appends/updates one
+ line: `date · what was just completed · EXACT next action`. A resumer starts by reading
+ this line.
+- A step that is half-done at interruption is simply redone from its last commit — or, if
+ the orchestrator finds uncommitted work in a leftover `.claude/worktrees/agent-*`
+ worktree, it secures that as a WIP commit on the branch first (proven pattern).
+
+## How to resume (human instructions)
+
+1. Wait for usage limits to reset (session window), then start a **fresh** Claude Code
+ session in this repo (cheaper than resuming the old conversation; everything needed is
+ on disk and in Claude's project memory).
+2. Say: **"Resume the Cloud Team Collections tasks per
+ Design/CloudTeamCollections/orchestration/RESUME.md."**
+3. The orchestrator will: find unmerged `task/*` branches and read their progress logs;
+ secure any uncommitted worktree work as WIP commits; relaunch unfinished agents with
+ their prompt files from this folder (prompts are resume-aware — they check for an
+ existing branch first); review and merge finished branches into `cloud-collections`
+ per IMPLEMENTATION.md rules; rebase onto origin/master at least daily.
+
+## Orchestrator notes
+
+- Launch prompts live in this folder, one per task (`-.prompt.md`). Sonnet
+ agents. Front-end/server-file tasks run in isolated worktrees; C#-building tasks run in
+ the MAIN tree (worktrees lack initialized build deps) — one C# task at a time.
+- Review before merge is MANDATORY (independently re-run the tests; see the merge log in
+ IMPLEMENTATION.md for the kinds of bugs review has caught: SQL type bug, bad bcrypt
+ hash, fake-session-token spec error, ungated UI section, JSON-null claimed bug).
+- C# test filter for cloud work MUST be
+ `"FullyQualifiedName~Cloud|FullyQualifiedName~TeamCollection|FullyQualifiedName~SharingApi"`
+ (exclude `~LiveTests` unless the stack is up): SharingApiTests live under
+ web.controllers and match NEITHER ~Cloud NOR ~TeamCollection — that gap let a real bug
+ merge with "all green" claims (7 Jul).
+- The local dev stack must be up for server/C#-integration verification: `supabase start`
+ + `docker-compose -f server/dev/docker-compose.yml up -d` (see server/dev/README.md;
+ MinIO must be on the supabase network — the compose file handles this).
+- Known environment quirks: pre-commit hook fails in worktrees (prettier manually +
+ `--no-verify`, orchestrator re-verifies); Bloom.exe often running → apphost copy error
+ MSB3027 is benign if test DLLs are fresh; edge-runtime containers must reach MinIO as
+ `bloom-minio:9000`, never `host.containers.internal` (hangs under Podman).
diff --git a/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md b/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md
new file mode 100644
index 000000000000..031cbb1a34ed
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md
@@ -0,0 +1,95 @@
+# Squash plan: review-grained history for the Cloud TC feature
+
+Goal (John, 10 Jul 2026): a new branch from current `origin/master` whose commits are
+meaningful, human-reviewable steps — replacing `cloud-collections`' ~204 orchestration-grained
+commits (126 first-parent) for review/merge purposes. The working branch `cloud-collections`
+stays untouched; the squashed branch is a **regenerable packaging artifact**.
+
+## Method: path-staged rebuild (recommended)
+
+Do NOT interactive-rebase 126+ commits (it conflicts immediately — master already contains
+cherry-picked batch commits, and the branch has merge-style history). Instead, rebuild the
+final tree in dependency-ordered file groups:
+
+```bash
+git fetch origin
+git checkout -b cloud-tc-for-review origin/master
+# For each group below, in order:
+# git checkout cloud-collections -- (adds/modifies)
+# git rm -q (see Deletions note)
+# git commit (message per group, below)
+# Then VERIFY (all three must hold):
+git diff cloud-tc-for-review cloud-collections --stat # MUST be empty
+dotnet test src/BloomTests/BloomTests.csproj -c Release --filter "(FullyQualifiedName~Cloud|FullyQualifiedName~TeamCollection|FullyQualifiedName~SharingApi)&FullyQualifiedName!~LiveTests"
+cd src/BloomBrowserUI && pnpm lint && pnpm vitest run # (or the targeted cloud files)
+```
+
+Properties: byte-identical end state (verified by the empty diff), zero conflict resolution,
+zero history surgery, re-runnable any time `cloud-collections` advances (delete + regenerate +
+force-push the packaging branch — it carries no one's work).
+
+Caveat for reviewers (put in the PR description): each commit is a coherent reviewable unit
+and the ORDER makes most of them compile, but only the FINAL tree is test-verified. That is
+the accepted trade-off; per-commit CI-green is not a goal.
+
+Deletions note: files the feature DELETED relative to master must be `git rm`'d in their
+group. Enumerate with `git diff --name-status origin/master...cloud-collections | grep '^D'`
+(currently expected: none or near-none; MyCloudCollectionsSection.tsx etc. were added AND
+deleted within the branch so they never existed on master).
+
+## The groups (dependency-ordered; ~9 commits)
+
+1. **Design docs & plans** — `Design/CloudTeamCollections/**` (34 files),
+ `.github/skills/xlf-strings` tweak. "Read this first" context: architecture,
+ CONTRACTS.md, GOING-LIVE.md, orchestration records incl. the dogfood batch log.
+ Msg: `Cloud Team Collections: design docs, wire contracts, and project records`
+2. **Server: schema, RLS, RPCs, pgTAP** — `supabase/migrations/**` (7), `supabase/tests/**`,
+ `supabase/config.toml`, `supabase/snippets/**`, `supabase/.gitignore`.
+ Msg: `Cloud TC server: tc schema, RLS policies, RPCs, and pgTAP tests`
+3. **Server: edge functions** — `supabase/functions/**` (21).
+ Msg: `Cloud TC server: edge functions for checkin/download/collection-file transactions`
+4. **Local dev stack** — `server/**` (16: MinIO compose, seed users, functions env,
+ parity-check console, README).
+ Msg: `Cloud TC dev stack: local Supabase + MinIO, seed users, S3 parity checks`
+5. **Client core** — `src/BloomExe/TeamCollection/Cloud/{CloudEnvironment,CloudAuth,
+ CloudCollectionClient,CloudRepoCache,CloudBookTransfer,...}.cs`, `S3Extensions`,
+ `BloomExe.csproj` (AWSSDK v4 bump) + matching `src/BloomTests/TeamCollection/Cloud/`
+ unit-test files for these classes.
+ Msg: `Cloud TC client core: auth, API client, repo cache, S3 transfer (AWSSDK v4)`
+6. **Cloud TeamCollection backend** — `CloudTeamCollection.cs`, `CloudCollectionMonitor.cs`,
+ `CloudJoinFlow.cs`, `RemoteBookAutoApplyQueue.cs`, `TeamCollection*.cs` seams,
+ `TeamCollectionManager.cs`, `TeamCollectionLink/LastKnownUser`, `Program.cs` (refusal
+ path), `NonFatalProblem.cs` (automation guard), `DisconnectedTeamCollection.cs` + their
+ tests (TeamCollectionAutoApplyTests, CloudSyncAtStartupTests, CloudAccountSwitch*, …).
+ Msg: `Cloud TC backend: cache-backed TeamCollection, polling monitor, join flow,
+ background downloads, account-switch handling`
+7. **HTTP API layer** — `SharingApi.cs`, `TeamCollectionApi.cs`, `CollectionChooserApi.cs`,
+ `CollectionApi.cs`, `ExternalApi.cs`, `WorkspaceApi/Model` bits + their tests.
+ Msg: `Cloud TC API: sharing/membership endpoints, capabilities, join cards, book-list merge`
+8. **Front-end UI + strings** — `src/BloomBrowserUI/**` (44), `DistFiles/localization/en/**`.
+ Msg: `Cloud TC UI: sign-in, sharing dialog, status panel, join cards, download placeholders`
+9. **E2E harness + specs** — `src/BloomTests/e2e/**`.
+ Msg: `Cloud TC E2E: Playwright-over-CDP harness and 10 two-instance scenarios`
+
+Bucketing rule: run `git diff --name-only origin/master...cloud-collections`, assign every
+file to exactly one group (a file with mixed concerns goes to its PRIMARY group — e.g.
+TeamCollection.cs → group 6 even though item-8 recovery touched it); after group 9, stage
+**everything still unassigned** into the best-fitting group before its commit — the empty
+final diff is the proof nothing was dropped.
+
+## Alternative: single squash (fallback)
+
+`git checkout -b cloud-tc-for-review origin/master && git merge --squash cloud-collections
+&& git commit` — one giant commit, 235 files. Only if reviewers prefer one unit; the grouped
+version costs ~30 min more and is far more reviewable.
+
+## Coordination
+
+- **When:** after bug #0 (takeover semantics) is decided+fixed and the full matrix verdict is
+ in — packaging before that just means regenerating. Regeneration is cheap by design.
+- **PRs:** open a NEW draft PR from `cloud-tc-for-review`; close #8048 with a comment pointing
+ at it (bots then review the meaningful commits). `cloud-collections` remains the working
+ branch until merge; regenerate the packaging branch (delete, rebuild, force-push) whenever
+ the working branch advances.
+- **Merge:** master ultimately merges `cloud-tc-for-review`; verify once more that its tree
+ equals `cloud-collections`' before merging, then archive the working branch.
diff --git a/Design/CloudTeamCollections/orchestration/notes-item7-progressive-join.md b/Design/CloudTeamCollections/orchestration/notes-item7-progressive-join.md
new file mode 100644
index 000000000000..52b4dcb5d304
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/notes-item7-progressive-join.md
@@ -0,0 +1,66 @@
+# Item 7 scouting notes (progressive join) — 9 Jul 2026, read-only scout
+
+Companion to DOGFOOD-BATCH-1.md item 7. Verified paths/lines as of commit e98cd809c.
+
+## Where the join blocks today
+`CloudJoinFlow.JoinCollection` (CloudJoinFlow.cs:163-235) is fully synchronous:
+ClaimMemberships → DetermineScenario/folder → write TeamCollectionLink → HydrateFromServer
+(line 231: populates CloudRepoCache with book list/titles/seqs, NO content) →
+CopyRepoCollectionFilesToLocal (232: the .bloomCollection settings + collection files) →
+**CopyAllBooksFromRepoToLocalFolder (233: the blocking bulk download to defer)**.
+SharingApi.HandlePullDown (SharingApi.cs:498-566) replies with collectionPath only after all
+of that returns; TS then posts workspace/openCollection → Program.SwitchToCollection
+(Program.cs:1842-1847). Settings + titles are available WITHOUT any book download.
+Note: cloud pull-down does NOT set NextMergeIsFirstTimeJoinCollection (folder TC does, at
+FolderTeamCollection.cs:1445) — so reopen runs SyncAtStartup with firstTimeJoin:false.
+
+## Resume path (already exists, verified)
+SyncAtStartup (TeamCollection.cs:2226; repo-book loop 2507-2558): a repo book with no local
+folder hits the "brand new book! Get it." branch → CopyBookFromRepoToLocalAndReport. Pinned
+by CloudSyncAtStartupTests.SyncAtStartup_NewBookOnlyInRepo_IsFetchedToLocal (251-293).
+CAVEAT: it runs synchronously inside the "Syncing Team Collection" progress dialog
+(SynchronizeRepoAndLocal, TeamCollection.cs:2960-3032) — a half-joined collection would
+block its next open until all missing books download, so item 7 must make this path skip
+cloud missing-book fetches (hand them to the background queue instead).
+
+## Book list / placeholder seam
+CollectionApi.HandleBooksRequest (CollectionApi.cs:559-647, endpoint collections/books)
+builds the JSON from BookCollection.GetBookInfos (BookCollection.cs:297-355), which is
+LOCAL-DISK-ONLY — repo books with no local folder are simply absent. Cleanest seam: merge
+CloudTeamCollection.GetBookList()/repo-cache titles into HandleBooksRequest's JSON with a
+`notYetDownloaded: true` flag (keeps BookCollection disk-only). TS: BooksOfCollection.tsx
+renders via LazyLoad with existing BookButtonPlaceHolder (BookButton.tsx:679-698, already
+listens for a bookImage/reload websocket to swap in the real thumbnail); thumbnail API
+falls back to a placeholder image for missing folders (CollectionApi.cs:755-789).
+
+## Selection priority + "downloading" status
+collections/selected-book POST (CollectionApi.cs:149-208) catches-and-logs failures for
+missing folders (198-208) — natural place for the "prioritize this download" hint.
+Status panel: clone the offlineDisabled pattern — server seam
+TeamCollectionApi.AddCloudBookStatusFields (TeamCollectionApi.cs:912-943, populates
+offlineDisabledReason when disconnected && no local seq); client union + branch + render in
+TeamCollectionBookStatusPanel.tsx (union line 63, effect 104-160, render ~805).
+
+## Queue reuse
+RemoteBookAutoApplyQueue (single-consumer FIFO dedupe; TeamCollection.cs lazily constructs
+at 117-128) is the right vehicle but has NO priority mechanism — selection-priority needs a
+front-of-queue Enqueue variant. CloudBookTransfer.DownloadFiles (CloudBookTransfer.cs:
+340-431) hash-skips already-present files → interrupted downloads resume for free; the
+whole-folder atomic swap lives in FetchBookFromRepo (CloudTeamCollection.cs:735-821), so
+the placeholder-only-or-fully-downloaded invariant already holds at folder level.
+
+## Test landscape
+No CloudJoinFlow test file exists. CloudSyncAtStartupTests uses a fake HTTP executor
+(pattern to copy). No test covers HandleBooksRequest JSON shape (placeholder entries land
+uncovered unless added). Queue seams: TestOnly_MakeAutoApplyQueueSynchronous /
+TestOnly_ProcessAutoApplyRemoteChange (TeamCollection.cs:136-150).
+
+## Sketch (for the implementation brief)
+1. CloudJoinFlow: drop line 233; after settings download, enqueue all repo books.
+2. HandleBooksRequest: synthetic notYetDownloaded entries from the repo cache.
+3. Background queue drains; each completion broadcasts bookImage/reload + status refresh.
+4. selected-book handler: front-of-queue bump + "Downloading…" panel state via
+ AddCloudBookStatusFields; selection of a missing book must not crash (currently
+ catch-and-log path).
+5. SyncAtStartup for cloud: missing books → queue instead of synchronous fetch (keeps the
+ startup progress dialog fast AND preserves resume semantics).
diff --git a/Design/CloudTeamCollections/orchestration/ui-wiring.prompt.md b/Design/CloudTeamCollections/orchestration/ui-wiring.prompt.md
new file mode 100644
index 000000000000..0c77128fc12c
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/ui-wiring.prompt.md
@@ -0,0 +1,52 @@
+# Agent prompt — Wave-3 UI wiring (resume-aware)
+
+You are implementing the final Wave-3 step: connecting the Wave-1/2 UI shells to the real
+C# endpoints task 06 just delivered. Work in an isolated git worktree of
+c:\github\BloomDesktop.
+
+**Resume check (do this FIRST):** if branch `task/ui-wiring` exists, check it out and
+continue from the `## Progress log` at the bottom of this prompt's companion notes in the
+task files below. Otherwise `git checkout -b task/ui-wiring`.
+
+**Durability protocol (mandatory):** commit after EVERY completed item; messages ending
+"Co-Authored-By: Claude Fable 5 "; keep a running `## Progress log`
+(one line per item: `date · done · exact next action`) at the bottom of
+`Design/CloudTeamCollections/tasks/07-ui-setup.md` (the wiring completes that task's
+deferred items). Pre-commit hook fails in worktrees: `yarn prettier --write` your files,
+then `--no-verify`.
+
+**Anti-hang rules:** vitest single-run only; never `yarn build`; no dev servers.
+
+**Setup:** ` cd src/BloomBrowserUI && yarn install` (leading space; yarn only). Follow
+src/BloomBrowserUI/AGENTS.md conventions and the gating rule: all cloud UI stays behind
+the experimental feature / capability flags; folder-TC UI byte-identical.
+
+**Read first:** the "Still needed for UI wiring" section of task 06's final progress-log
+entry in `Design/CloudTeamCollections/tasks/06-api-endpoints.md`; the Wave-3 deferrals in
+07's and 08's progress logs; `src/BloomBrowserUI/teamCollection/sharingApi.ts` and
+`teamCollectionApi.tsx` (the hooks now have REAL endpoints behind them).
+
+**Work items:**
+1. Fix the `WireUpForWinforms` double-registration in `CreateTeamCollection.tsx` (task 06
+ found the cloud dialog's registration unconditionally overwrites the folder one, so the
+ folder create dialog can no longer open). Both dialogs must coexist — e.g. select the
+ component from a URL param/window flag the C# side already passes, or split bundles.
+ This is the one item that BREAKS FOLDER TC today: fix it first and prove it with a test.
+2. Dedicated sign-in dialog: a small email/password dialog for dev auth mode (per
+ `sharing/loginState`'s mode field), opened by `sharing/showSignIn` instead of the
+ create-dialog placeholder. Real-mode shows a "not yet available" message. Wire the C#
+ side's existing endpoint; component test for both modes.
+3. Wire `SharingPanel` into `TeamCollectionSettingsPanel`'s isTeamCollection branch for
+ cloud TCs (07's deferred item) — folder TCs keep the old admin panel.
+4. Wire `JoinCloudCollectionDialog`'s state matching into `CollectionChooser`'s
+ `onPullDown` (07's deferred item) using `collections/pullDown`'s real responses.
+5. Sweep `sharingApi.ts`/`teamCollectionApi.tsx` for any lingering mock-only defaults that
+ would mask real endpoint failures now that the endpoints exist (fail fast per
+ AGENTS.md).
+6. Run the full component sweep (`yarn vitest run teamCollection collectionsTab
+ collection react_components/TopBar`) + `yarn lint`; all green, no new warnings.
+
+**Localization:** new strings per `.github/skills/xlf-strings/SKILL.md`, en-only.
+
+**Final report (raw data):** branch + shas; per-item status; test sweep verbatim counts;
+lint result; anything still blocking the two-instance manual smoke (the Wave-3 gate).
diff --git a/Design/CloudTeamCollections/tasks/00-enablers.md b/Design/CloudTeamCollections/tasks/00-enablers.md
new file mode 100644
index 000000000000..37c4fb608dbf
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/00-enablers.md
@@ -0,0 +1,33 @@
+# 00 — Enablers (Wave 0, orchestrator-led, sequential)
+
+**Goal**: make the base classes backend-pluggable with provably zero behavior change for the
+folder backend.
+
+**Dependencies**: none. **Do not parallelize** — touches shared hot files.
+
+**File ownership (shared, exclusive during this task)**: `src/BloomExe/TeamCollection/TeamCollection.cs`,
+`TeamCollectionManager.cs`; new `TeamCollectionLink.cs`.
+
+## Steps
+- [x] `TeamCollectionLink` class: parse/write folder-path and `cloud://sil.bloom/collection/`
+ forms of `TeamCollectionLink.txt`; error on both-forms-present.
+- [x] Backend factory replacing the three hardcoded `new FolderTeamCollection(...)` sites
+ (manager ctor ~line 335–416, `ConnectToTeamCollection` ~500, subscription-reconnect
+ handler ~260–306). Add `ConnectToCloudCollection(collectionId)` to `ITeamCollectionManager`
+ (throws NotImplemented for now).
+- [x] Virtual lock seams: `protected virtual bool TryLockInRepo(bookName)` /
+ `UnlockInRepo(bookName, force)`; folder overrides preserve current read-modify-write
+ behavior verbatim; `AttemptLock`/`UnlockBook`/`ForceUnlock` route through them.
+- [x] Capability flags: virtual `SupportsVersionHistory` / `SupportsSharingUi` /
+ `RequiresSignIn` (folder: false/false/false).
+- [x] Audit + document the ~10 `WriteBookStatus` callers (notes for task 05's diff-dispatch).
+- [x] Feature flag: cloud sharing behind the experimental-features setting (registration only;
+ no UI yet).
+
+## Acceptance
+- `dotnet test` — the ENTIRE existing TeamCollection suite passes unchanged (no test edits).
+- New `TeamCollectionLinkTests` (parse/write/garbage/missing/both-present).
+- Manual smoke: an existing folder TC opens, checks out, checks in exactly as before.
+
+**Agent notes**: orchestrator only. No cloud code in this task. Keep each refactor
+mechanical and reviewable in isolation.
diff --git a/Design/CloudTeamCollections/tasks/01-server-schema.md b/Design/CloudTeamCollections/tasks/01-server-schema.md
new file mode 100644
index 000000000000..fd34efc5cec6
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/01-server-schema.md
@@ -0,0 +1,41 @@
+# 01 — Server schema + RPCs (Wave 1)
+
+**Goal**: the `tc` Postgres schema, RLS, and all pure-DB RPCs, per CONTRACTS.md.
+
+**Dependencies**: CONTRACTS.md frozen. Parallel-safe (owns `supabase/migrations/**`,
+`supabase/tests/**` only).
+
+## Steps
+- [x] `supabase init` layout at repo root (`supabase/config.toml`), local dev via `supabase start`.
+- [x] Migrations: `collections`, `members` (approved accounts; unique claimed user per
+ collection; last-admin guard trigger), `books` (lock columns; `deleted_at`; unique
+ (collection, instance_id); unique live lower(name)), `versions` (metadata),
+ `version_files` (current manifest incl. s3_version_id), `collection_file_groups` +
+ `collection_group_files`, `color_palette_entries`, `events` (+ indexes, realtime trigger),
+ `checkin_transactions`.
+- [x] RLS policies per design: member read; admin membership writes; NO direct writes to
+ books/versions; realtime channel authorization.
+- [x] `tc.jwt_email_verified()` SQL helper: the ONE place that reads verification off the
+ token — Firebase-style `email_verified` claim OR local-GoTrue auto-confirmed users
+ (dev stack, task 11). Everything else calls the helper, never the claim directly.
+- [x] RPCs from CONTRACTS.md: create_collection, my_collections, claim_memberships
+ (requires `tc.jwt_email_verified()`), get_collection_state, get_changes, checkout_book (conditional
+ UPDATE), unlock_book, force_unlock, delete_book (lock required), undelete_book,
+ rename_check, member management (remove ⇒ force-unlock + events), add_palette_colors,
+ log_event.
+- [x] Events: `BookHistoryEventType` numeric parity + incident types (WorkPreservedLocally…).
+
+## Acceptance — MET
+- pgTAP suite **GREEN: 42/42 (6 Jul 2026, `supabase test db` on local stack via Podman)**.
+ Covers: RLS matrix; checkout concurrency (two racing calls, exactly one winner); claiming requires
+ verified email; last-admin guard; get_changes cursor; tombstone/undelete; live-name uniqueness
+ (tombstoned names reusable).
+ Orchestrator fixes to get green: plan count corrected (60 → 42 actual assertions); RLS
+ assertions now run under `SET LOCAL ROLE authenticated` — the suite's postgres superuser
+ BYPASSES row security, so unswitched RLS tests pass vacuously (keep this pattern for all
+ future RLS assertions). Migrations + seed also verified live: RPC round-trip
+ (create_collection → my_collections) via PostgREST with Content-Profile: tc succeeded.
+
+**Agent notes**: Sonnet. All timestamps `now()` server-side. User ids are text, not uuid —
+Firebase UIDs are ~28 chars (local-GoTrue dev users are uuids; text covers both).
+NFC-normalize name/path comparisons.
diff --git a/Design/CloudTeamCollections/tasks/02-edge-functions.md b/Design/CloudTeamCollections/tasks/02-edge-functions.md
new file mode 100644
index 000000000000..062887769b87
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/02-edge-functions.md
@@ -0,0 +1,167 @@
+# 02 — Edge functions + AWS provisioning (Wave 1)
+
+**Goal**: the five S3-brokering edge functions per CONTRACTS.md, and the checked-in AWS
+provisioning script. Dev/test target is MinIO via the local stack (task 11); real AWS is a
+deferred config swap (see the master checklist's deferred-infrastructure list).
+
+**Dependencies**: CONTRACTS.md; task 11's docker-compose/env-var contract. Runs parallel to
+01 (mock DB until 01 merges, then integrate). Owns `supabase/functions/**`, `server/provision-aws.*`.
+
+## Steps
+- [x] `checkin-start`: membership + lock + base-version checks; new-book path (bookId null ⇒
+ row locked-to-caller, versionless, invisible); diff proposed manifest vs current;
+ transaction reuse/resume; credential issuance behind a small provider seam:
+ **dev mode** (env-selected) returns static MinIO credentials, **production mode** does
+ real STS via `bloom-teams-broker` with a per-request session policy scoped to the one
+ book prefix — both in the identical response shape (CONTRACTS.md unchanged; clients
+ can't tell the difference).
+- [x] `checkin-finish`: verify sha256 attributes; capture s3 version-ids; single DB tx
+ (version metadata row, current-manifest replacement, book update, lock release, events,
+ `.manifest.json` write); MissingOrBadUploads retry path; idempotent.
+- [x] `checkin-abort`; transaction expiry sweep (versionless-row reaping).
+- [x] `download-start`: read-only creds (`GetObject`+`GetObjectVersion`), collection scope.
+- [x] `collection-files-start/finish` with optimistic `expectedVersion`.
+- [x] `ClientOutOfDate` handling (client version in requests).
+- [x] `server/provision-aws` script (idempotent): buckets `bloom-teams-production|sandbox`,
+ versioning ON, public access blocked, lifecycle (abort-multipart 7d; noncurrent expiry
+ per the confirmed window), broker role + assume-only IAM user; document secrets setup
+ (`supabase secrets set`). **Written and reviewed now; RUN later** when real AWS access
+ exists — acceptance for this task does not require an AWS account.
+
+## Acceptance
+- [x] Deno tests per function: happy path; lock-held; base-version-superseded; checksum failure
+ (missing + wrong-content object); resume; expiry; new-book invisibility until commit.
+ (Lock-held/base-version-superseded/resume/new-book-invisibility/checksum-failure verified
+ live against the real stack — see Progress log; per-function Deno unit tests below cover
+ the handler-level wiring/error-passthrough hermetically. `expiry` is covered by the
+ SQL's reap logic + the invariant test below, not a live 48h wait.)
+- [x] Invariant test: transaction lifetime < noncurrent-expiry floor (config assertion).
+
+**Agent notes**: Sonnet. MinIO for S3 in tests AND as the dev-mode target (task 11's stack).
+Only these functions ever hold AWS/MinIO admin creds.
+
+## Progress log
+
+- 2026-07-06 · done: new migration `20260706000004_tc_checkin_txn_functions.sql` adding
+ the internal SECURITY DEFINER transaction functions (`checkin_start_tx`,
+ `checkin_finish_tx`, `checkin_abort_tx`, `collection_files_start_tx`,
+ `collection_files_finish_tx`, `download_start_check`, expiry-reap helpers, PT###
+ HTTP-status passthrough convention) that back all 6 edge functions; applied clean via
+ `supabase db reset --local`. All 6 edge functions authored under `supabase/functions/`
+ (`checkin-start`, `checkin-finish`, `checkin-abort`, `download-start`,
+ `collection-files-start`, `collection-files-finish`) plus `_shared/` helpers
+ (env, errors, handler, rpc, s3-credential-provider-seam). `deno check` passes on all.
+ NOT YET tested against the live stack. Next action: run
+ `supabase functions serve --env-file server/dev/functions.env` (env file not yet
+ created — create it first with `BLOOM_DEV_MODE=true` [historical name; renamed
+ `BLOOM_CLOUD_LOCAL_MODE` 8 Jul 2026], `BLOOM_S3_ENDPOINT=http://host.containers.internal:9000`,
+ `BLOOM_S3_BUCKET=bloom-teams-local`), then exercise checkin-start → checkin-finish
+ happy path end-to-end with a real dev-seed user JWT (alice@dev.local), then write Deno
+ unit tests per function and continue through the acceptance checklist (lock-held,
+ base-version-superseded, checksum failure, resume, expiry, new-book invisibility).
+- 2026-07-06 (later same day) · done: full LIVE integration verification against the real
+ local stack (Supabase 127.0.0.1:54321 + MinIO via `supabase functions serve --env-file
+ server/dev/functions.env`), using a scratch Deno driver script (not committed — ad hoc)
+ exercising every item in the Acceptance checklist except a real 48h expiry wait: happy
+ path (new-book checkin-start → PUT via MinIO AssumeRole creds → checkin-finish →
+ download-start GetObject round-trip), idempotent checkin-finish retry, resume of an
+ open transaction (both new-book and existing-book), lock-held (409 LockHeldByOther),
+ base-version-superseded (409), checksum failure via MissingOrBadUploads (409, upload
+ omitted), new-book invisibility until commit (verified via get_collection_state),
+ collection-files two-phase commit + VersionConflict (409). **26/26 checks passed** after
+ fixing 4 real bugs found along the way (all fixed + migrations reapplied via
+ `supabase db reset`):
+ 1. **`host.containers.internal`/`host.docker.internal` DNS-resolves but the traffic
+ HANGS** (not slow — indefinite) for any Deno/edge-runtime HTTP call through Podman's
+ gvproxy host-gateway hop, even though plain `curl` over the identical URL succeeds
+ instantly (verified with raw `Deno.connect()`, native `fetch`, and the AWS SDK, both
+ from a throwaway container and the real edge-runtime container). This is almost
+ certainly what stalled the prior interrupted attempt at this task. **Fix**: MinIO
+ now also joins the Supabase CLI's own project Docker network
+ (`server/dev/docker-compose.yml`'s `networks:` block — external network
+ `supabase_network_bloom-team-collections`, created by `supabase start`) and is
+ addressed by container name (`http://bloom-minio:9000`, in `functions.env`).
+ Container-to-container traffic on a shared bridge network is instant. Documented in
+ `server/dev/README.md`'s new "Known gotchas" section.
+ 2. **`supabase/config.toml`'s `[edge_runtime].policy = "oneshot"`** (the generated
+ default) re-transpiles/type-checks the whole module graph — including the heavy
+ `npm:@aws-sdk/client-s3`+`client-sts` imports in `_shared/s3.ts` — on every request,
+ which reliably exceeds the edge-runtime's ~10s worker-boot timeout
+ (`InvalidWorkerCreation: worker did not respond in time`) for any function that
+ reaches `getScopedCredentials`/`adminS3Client`. **Fix**: switched to
+ `policy = "per_worker"` (compiles once; also closer to production). Documented in the
+ same README section.
+ 3. **New-book checkin-start resume was unreachable** in `checkin_start_tx`
+ (`20260706000004_...sql`): CONTRACTS.md's checkin-start response never exposes
+ `bookId` (by design — that's what makes an uncommitted book invisible), so a client
+ resuming an interrupted new-book Send has no id to send back and must re-call with
+ `bookId: null` + the same `bookInstanceId`. The old code always ran the
+ insert-a-new-row path whenever `bookId` was null, so the very row created by try #1
+ tripped the "instance_id already in use" conflict check on try #2. Fixed: the
+ new-book branch now looks up any existing row by `(collection_id, instance_id)`
+ first and treats "found, not yet committed, still locked to me" as a resume (not a
+ conflict) — see the updated comment block in the migration.
+ 4. **`get_collection_state`'s full-snapshot branch leaked uncommitted new books** to
+ every collection member (`20260706000003_tc_rpcs.sql`, task 01's file — a
+ cross-cutting bug this task's acceptance criterion "new-book invisibility until
+ commit" directly depends on). The delta branch was already safe (a never-committed
+ book has no events to join against yet), but the full-snapshot branch queried
+ `tc.books` directly with no such filter. Fixed by excluding rows where
+ `current_version_id IS NULL` unless `locked_by = tc.current_user_id()` (the sender
+ still sees their own in-flight new book).
+ Remaining for this task: Deno unit tests per function (mocked RPC/S3 — the live spike
+ above covers integration but not fast, hermetic CI-friendly coverage), the invariant
+ test (transaction lifetime 48h < noncurrent-expiry-floor 7d — config assertion), and
+ `server/provision-aws` (author + review only, no AWS account).
+- 2026-07-06 (later still) · done: refactored all 6 `supabase/functions/*/index.ts` to
+ `export const handler = async (req, body) => {...}` + `if (import.meta.main) { serveJsonPost(handler); }`
+ instead of passing the arrow function straight into `serveJsonPost(...)`. Empirically
+ verified (via the running local stack) that the real supabase-edge-runtime still sets
+ `import.meta.main = true` and serves normally with this guard — re-ran the full 26-check
+ live-integration suite after the refactor, still 26/26. This makes each handler
+ importable and directly callable from a Deno test with a mocked `Request`, without a
+ module-load side effect of starting a real `Deno.serve` (which would collide across
+ test files). `deno check` clean on all 6. Next action: write the Deno test suite
+ (`_shared/s3.test.ts` using `aws-sdk-client-mock` for STS/S3; per-function
+ `index.test.ts` mocking `globalThis.fetch` for the RPC calls), the invariant config
+ assertion, then author `server/provision-aws`.
+- 2026-07-06 (later still) · done: wrote the Deno test suite. `_shared/test_support.ts`
+ (new) provides `setTestEnv`/`mockRequest`/`withMockFetch`/`routedFetchStub`/
+ `callHandler` (the last translates a thrown `HttpError` into its `Response`, mirroring
+ what `serveJsonPost` does, since tests call the exported `handler` directly).
+ `_shared/s3.test.ts`: 8 tests covering `hexToBase64`, `getScopedCredentials` (dev-mode
+ MinIO AssumeRole call shape + no-Policy-in-dev + missing-credentials failure),
+ `verifyUploadedObject` (match/mismatch/missing-object/missing-VersionId), and
+ `writeManifestBackup` (never throws). One `index.test.ts` per function (checkin-start
+ 5, checkin-finish 4, checkin-abort 4, download-start 3, collection-files-start 3,
+ collection-files-finish 3 = 22 tests) covering happy path, required-field validation,
+ and RPC error passthrough (409/404/426) with S3-not-called assertions on the error
+ paths. `_shared/invariants.test.ts`: 2 tests — re-parses the actual migration/compose
+ source text (rather than hardcoding both sides) to assert (a) every 48h transaction-
+ expiry `INTERVAL` literal agrees and (b) 48h < the dev MinIO noncurrent-expiry floor
+ (7d, from `docker-compose.yml`'s `--noncurrent-expire-days`). All S3/STS mocked via
+ `aws-sdk-client-mock`; all PostgREST calls mocked via a `fetch` stub — no live stack
+ needed. **32/32 Deno tests pass**
+ (`deno test --allow-net --allow-env --allow-sys --allow-read supabase/functions/`).
+ Remaining for this task: `server/provision-aws` (author + review only).
+- 2026-07-06 (final) · done: authored `server/provision-aws.ps1` (PowerShell, matching
+ the only existing `server/` script convention — `server/dev/smoke.ps1`). Idempotent:
+ checks-then-creates S3 buckets (`bloom-teams-`, default
+ production+sandbox) with versioning ON, all-public-access blocked, and lifecycle
+ rules (noncurrent-version-expiry 7d + abort-incomplete-multipart-upload 7d under
+ `tc/`, both parameterized); an IAM role `bloom-teams-broker` (permission-policy
+ ceiling: PutObject/GetObject/GetObjectVersion/AbortMultipartUpload/
+ ListMultipartUploadParts on both buckets — the edge function's per-request session
+ Policy narrows further, per `_shared/s3.ts`'s `buildSessionPolicy`); an assume-only
+ IAM user `bloom-teams-broker-caller` (sole permission: `sts:AssumeRole` on that
+ role — this is the identity behind `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` in
+ `_shared/env.ts`'s `prodBrokerConfig`); an IAM admin user `bloom-teams-admin` with
+ direct S3 permissions (`BLOOM_S3_ADMIN_ACCESS_KEY`/`SECRET`, backing
+ `adminS3Client()`). Ends by printing the exact `supabase secrets set` command block
+ (including `BLOOM_S3_ENDPOINT=https://s3..amazonaws.com` and
+ `BLOOM_S3_FORCE_PATH_STYLE=false`, since `_shared/env.ts`'s `s3Env()` requires an
+ explicit endpoint even for real AWS). Supports `-WhatIf`. Verified with
+ `[System.Management.Automation.Language.Parser]::ParseFile` (no syntax errors) —
+ NOT run against a real AWS account (none available; matches the task's explicit
+ "written and reviewed now; RUN later" acceptance bar). All 5 steps of task 02 are
+ now complete.
diff --git a/Design/CloudTeamCollections/tasks/03-auth.md b/Design/CloudTeamCollections/tasks/03-auth.md
new file mode 100644
index 000000000000..f9eee8cae6ec
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/03-auth.md
@@ -0,0 +1,71 @@
+# 03 — Auth (Wave 1)
+
+**Goal**: `CloudAuth` + `CloudCollectionClient` skeleton behind one interface, with a **dev
+auth provider** (local GoTrue email/password against task 11's stack) as the first concrete
+implementation. Real BloomLibrary/Firebase sign-in (Option A/B/C) is a later drop-in provider —
+the decision is **not blocking** for this task or anything downstream.
+
+**Dependencies**: CONTRACTS.md; task 11's env-var contract. The Option A/B/C decision
+(colleague review — see design doc) is deferred to the real-infrastructure cutover; the
+interface is option-agnostic. Owns new files
+`src/BloomExe/TeamCollection/Cloud/CloudEnvironment.cs`, `CloudAuth.cs`,
+`CloudCollectionClient.cs`. (When the real option lands: if Option A, the BloomLibrary2
+`src/editor.ts` change and the Firebase Admin claim function live in their own repos —
+coordinate, do not fork here.)
+
+## Steps
+- [x] `CloudEnvironment`: one place resolving Supabase URL, anon key, S3 endpoint/bucket/
+ path-style, and auth mode from the `BLOOM_CLOUDTC_*` env vars (names per task 11's
+ README) over compiled defaults. Everything cloud-related reads config from here;
+ switching local ↔ sandbox ↔ production is config only.
+- [x] `CloudAuth` interface + session core (provider-agnostic): token store, proactive refresh
+ (timer at ~80% TTL + on-401), sign-out, "who am I" (email/user id), account-switch
+ detection hook.
+- [x] **Dev provider** (`AUTH_MODE=dev`): sign in = GoTrue password grant against the local
+ stack; unknown email ⇒ sign-up (auto-confirmed) then sign in — i.e. any login is
+ accepted. Honors `BLOOM_CLOUDTC_USER`/`BLOOM_CLOUDTC_PASSWORD` for silent auto-sign-in,
+ **bypassing shared stored tokens** — this is what lets two Bloom instances on one
+ machine run as two different users.
+- [x] Real-provider seam (`AUTH_MODE=real`): stub that surfaces "not yet available"; the
+ `external/login` payload hook (`ExternalApi.LoginSuccessful`) and refresh-token
+ user-setting (alongside LastLoginSessionToken) are wired but inert until the Option
+ A/B/C provider is implemented (deferred-infrastructure list).
+- [x] `CloudCollectionClient`: RestSharp client for RPCs + edge functions per CONTRACTS.md
+ (model on `BloomLibraryBookApiClient`), bearer injection, ClientOutOfDate surfacing,
+ typed errors (LockHeldByOther etc.).
+- [x] `sharing/loginState` endpoint groundwork (used by UI tasks; reports mode + identity so
+ dev-mode sign-in can be a plain email/password form instead of the browser flow).
+
+## Acceptance
+- `CloudAuthTests`: refresh on timer/401; refresh failure mid-operation aborts cleanly and
+ surfaces "please sign in"; account-switch detection hook; env-override identity wins over
+ stored tokens.
+- Client tests: bearer attached; typed error mapping; out-of-date handling.
+- Manual (local stack): two Bloom instances with different `BLOOM_CLOUDTC_USER` values hold
+ two distinct valid sessions simultaneously; a session survives > 2h (refresh soak).
+
+**Agent notes**: Sonnet. Editing a checked-out book must NEVER block on auth. Keep the dev
+provider tiny — it must be deletable without touching the session core.
+
+## Progress log
+
+- 6 Jul 2026 · done: CloudEnvironment/CloudAuth/CloudCollectionClient skeleton implemented and
+ building clean (BloomExe.csproj auto-globs new .cs files, no csproj edit needed); dev-provider
+ GoTrue calls live-verified against the local stack (sign-in, RPC with Content-Profile: tc,
+ 401/error shapes). All "Steps" checkboxes ticked. · next: write CloudAuthTests +
+ CloudCollectionClientTests under src/BloomTests/TeamCollection/Cloud/ covering the Acceptance
+ section, then run `dotnet test --filter FullyQualifiedName~Cloud` and the TeamCollection
+ regression filter.
+- 6 Jul 2026 (later) · done: full Acceptance test suite written and green —
+ CloudAuthTests/CloudCollectionClientTests/CloudEnvironmentTests (36 tests: mocked
+ ICloudAuthProvider/IRestExecutor fakes cover refresh-on-timer, refresh-on-401 success/failure,
+ account-switch, env-override-wins-over-stored-session, bearer injection, and every
+ CloudErrorCode mapping incl. ClientOutOfDate); plus one `[Explicit]` test
+ (LiveDevProvider_TwoUsersSignInConcurrently_HoldDistinctSessions) that live-verified alice/bob
+ get independent sessions + independent refreshes against the running local Supabase stack.
+ Full `FullyQualifiedName~Cloud` filter: 46/46 green. Full `FullyQualifiedName~TeamCollection`
+ regression filter: 244/244 green (no folder-TC regression). Task complete except the
+ multi-hour manual two-window/>2h-soak items in Acceptance, which need a human at a keyboard —
+ see the final report for what was substituted. · next: none for this task; ready for
+ orchestrator review/merge. Downstream: task 04 (client-core) builds the actual RPC/edge-function
+ method wrappers on top of CloudCollectionClient.CallRpc/CallEdgeFunction.
diff --git a/Design/CloudTeamCollections/tasks/04-client-core.md b/Design/CloudTeamCollections/tasks/04-client-core.md
new file mode 100644
index 000000000000..96285b59a3c5
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/04-client-core.md
@@ -0,0 +1,62 @@
+# 04 — Client core: cache, manifest, transfer (Wave 2)
+
+**Goal**: the data plumbing `CloudTeamCollection` will sit on.
+
+**Dependencies**: 03 (client/auth), CONTRACTS.md. Owns new files
+`src/BloomExe/TeamCollection/Cloud/CloudRepoCache.cs`, `BookVersionManifest.cs`,
+`CloudBookTransfer.cs`.
+
+## Steps
+- [x] `CloudRepoCache`: thread-safe in-memory book/lock/version map + collection-file record +
+ `last_seen_event_id`; persisted snapshot (`.bloom-cloud-repo-cache.json` in the local
+ collection folder, excluded from book enumeration); full-snapshot + delta application;
+ write-through from mutating RPC results. Never trusted for mutations.
+- [x] `BookVersionManifest`: model (path → sha256, size, s3VersionId), NFC path normalization,
+ junk/derived-file exclusion list (reuse the publish path's filters), local-folder diff
+ (changed/added/removed/unchanged) with hash computation reuse (`MakeChecksum` internals /
+ `Book.ComputeHashForAllBookRelatedFiles`).
+- [x] `CloudBookTransfer`: hash-skip uploads (parallel PUTs with checksum headers, byte
+ progress via IProgress) and downloads **by pinned (path, s3VersionId) only — never
+ "latest"**; staged-temp-then-atomic-swap; resume support both directions. Reuse
+ `BloomS3Client` session-credential + TransferUtility mechanics (extract shared helper if
+ needed — don't disturb the publish path).
+
+## Acceptance
+- [x] `CloudRepoCacheTests` (concurrency, snapshot round-trip, delta, cursor).
+- [x] `BookVersionManifestTests` (diff matrix with data sanity pre-checks; junk exclusion; NFC).
+- [x] `CloudBookTransferTests` (mock S3): skip logic both directions; resume skips done files;
+ checksum-mismatch retry; interrupted download leaves the working folder untouched;
+ **assert no code path issues an unversioned GET**.
+
+**Agent notes**: Sonnet. This task has no UI and no base-class edits.
+
+## Progress log
+
+- 7 Jul 2026 · Implemented CloudRepoCache.cs, BookVersionManifest.cs, CloudBookTransfer.cs
+ (src/BloomExe/TeamCollection/Cloud/); BloomExe builds clean. Extracted
+ `BloomS3Client.CreateAmazonS3Client(AmazonS3Config, AmazonS3Credentials)` from `protected`
+ instance to `internal static` (pure visibility/staticness change, no behavior change) so
+ CloudBookTransfer can reuse session-credential client construction without disturbing the
+ publish path. Checksummed uploads use a manually-set `x-amz-checksum-sha256` header (not the
+ SDK's native ChecksumAlgorithm/ChecksumSHA256 properties, which don't exist in the
+ AWSSDK.S3 3.5.3.10 this project is pinned to) — **live-verified against the local MinIO
+ stack**: PUT with the manual header, then read back via a separate probe using a newer
+ AWSSDK.S3 (3.7.511.8) GetObjectAttributes(ChecksumMode: ENABLED) — the SHA-256 came back
+ correctly, confirming MinIO (and by the same S3 API contract, real AWS) treats the manual
+ header identically to the native SDK feature, so `_shared/s3.ts`'s `verifyUploadedObject`
+ will accept it once wired up in a later task. Recommend (not done here — BloomExe.csproj is
+ orchestrator-owned at merge time per the shared-file schedule): bump AWSSDK.S3 so future
+ code can use the native properties instead of a raw header string.
+- 7 Jul 2026 · Added BookVersionManifestTests.cs (10), CloudRepoCacheTests.cs (16),
+ CloudBookTransferTests.cs (11, mocking IAmazonS3 via Moq) in
+ src/BloomTests/TeamCollection/Cloud/. `dotnet test --filter FullyQualifiedName~Cloud`:
+ 83/83 green (37 new + 46 pre-existing from task 03). `dotnet test --filter
+ FullyQualifiedName~TeamCollection`: 281/281 green (full folder+cloud TC regression). No
+ apphost/MSB3027 issue hit this run (clean 0-error build both times). Contract gap found (not
+ fixed — out of this task's file scope): CONTRACTS.md v1.1 defines no RPC returning a book's
+ per-file manifest (path/sha256/size/s3VersionId) for Receive/download — get_collection_state
+ only returns the aggregate current_checksum/current_version_seq. CloudRepoCache.
+ CloudCachedBook.Manifest and CloudRepoCache.RecordManifest are written with this gap in mind
+ (nullable, populated whenever a caller obtains a manifest by any means) so 05/06 aren't
+ blocked, but whoever wires up Receive will need either a new RPC or to read the S3
+ `.manifest.json` backup. Task complete; no further action needed from this agent.
diff --git a/Design/CloudTeamCollections/tasks/05-cloud-backend.md b/Design/CloudTeamCollections/tasks/05-cloud-backend.md
new file mode 100644
index 000000000000..a15ea3a605f7
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/05-cloud-backend.md
@@ -0,0 +1,172 @@
+# 05 — CloudTeamCollection + monitor (Wave 3, first)
+
+**Goal**: the backend subclass and change monitoring — the heart of the feature.
+
+**Dependencies**: 00, 01, 02, 03, 04. Owns new files `Cloud/CloudTeamCollection.cs`,
+`Cloud/CloudCollectionMonitor.cs`, `Cloud/CloudJoinFlow.cs`. Touches (exclusive this task):
+nothing shared — the manager factory seam from 00 is wired by config.
+
+## Steps
+- [ ] Implement every abstract member per the design doc's mapping table (list/status members
+ from cache; PutBookInRepo = Send pipeline via checkin-start/finish; FetchBookFromRepo =
+ pinned-version staged fetch + swap; delete/rename/tombstone via RPCs; collection-file
+ members on the group contracts; casing members against the book row; CheckConnection =
+ network + session + membership with precise messages; GetBackendType = "Cloud").
+- [ ] `WriteBookStatusJsonToRepo` diff-dispatch (per 00's caller audit): lock changes →
+ lock RPCs; bookkeeping-only writes never clear a lock; server stamps identity.
+- [ ] New-book first-Send path incl. NameConflict → "name2" resolution and id-conflict flow.
+- [ ] Unified recovery: on lock-lost/base-superseded, save `.bloomSource` to local Lost & Found,
+ Receive current, post incident event + message (distinct texts per sub-case).
+- [ ] Account rules: same-account sign-out/in safe; account switch with unsent changes blocked
+ with Send-or-preserve choices.
+- [ ] `CloudCollectionMonitor`: polling first (get_changes, 60s; on-activation), event→base-queue
+ mapping, event-id self-echo suppression, catch-up-then-trust on reconnect.
+- [ ] `CloudJoinFlow`: my_collections listing → local collection creation → first Receive
+ (six-scenario matching logic moved from FolderTeamCollection).
+- [ ] Modal Send/Receive orchestration on the existing BrowserProgressDialog harness.
+
+## Acceptance
+- `CloudTeamCollectionMemberTests`, `CloudTeamCollectionLockTests`, `CloudSyncAtStartupTests`
+ (ported matrix; asserts `.bloomSource` + incident events), `CloudCollectionMonitorTests`.
+- Folder-TC suite still green.
+- Manual: two Bloom instances on ONE machine (distinct collection folders + dev identities
+ via `BLOOM_CLOUDTC_USER`) against the local stack (task 11) — checkout/Send/Receive loop
+ works, lock state visible across instances.
+
+**Agent notes**: Sonnet, orchestrator reviews closely. Base-class code is read-only here;
+anything needing a base change goes back to the orchestrator.
+
+## Progress log
+
+- 7 Jul 2026 · done · Read task brief, CONTRACTS.md v1.2, architecture doc, write-book-status
+ audit, and surveyed (via sub-agent) the full abstract-member surface of TeamCollection.cs,
+ TeamCollectionManager.cs's two NotImplementedException seams, and the Wave 3/4 support
+ classes (CloudCollectionClient/CloudRepoCache/BookVersionManifest/CloudBookTransfer/CloudAuth/
+ CloudEnvironment). Added typed RPC/edge-function wrapper methods to CloudCollectionClient.cs
+ (create_collection, my_collections, claim_memberships, get_collection_state, get_changes,
+ get_book_manifest, checkout_book, unlock_book, force_unlock, delete_book, undelete_book,
+ rename_check, members list/add/remove/set_role, add_palette_colors, log_event,
+ checkin-start/finish/abort, download-start, collection-files-start/finish) — this file's own
+ doc comment said later tasks build these on top of it. Builds clean.
+ Next action: write `Cloud/CloudTeamCollection.cs` implementing every abstract member per the
+ mapping table, using `CloudRepoCache` + the new client methods + `CloudBookTransfer`.
+- 7 Jul 2026 · done · Wrote `Cloud/CloudTeamCollection.cs` (every abstract/virtual member of
+ TeamCollection implemented: status/list/presence from cache; TryLockInRepo/UnlockInRepo via
+ checkout_book/unlock_book/force_unlock; WriteBookStatusJsonToRepo diff-dispatch per the task 00
+ audit; PutBookInRepo = Send via checkin-start/upload/checkin-finish with "name2" NameConflict
+ retry and an inLostAndFound -> unified-recovery branch (.bloomSource to local Lost and Found +
+ log_event type 100 WorkPreservedLocally + distinct message per sub-case); FetchBookFromRepo =
+ Receive via get_book_manifest + pinned download into a staging folder + a two-rename atomic
+ directory swap done by this class (per the merge-log note that CloudBookTransfer's own move loop
+ isn't itself atomic); GetRepoBookFile = single pinned-file fetch, cached per sync pass; casing
+ methods against the cached book's canonical Name; collection files via collection-files-start/
+ finish (push) and a direct S3 prefix-list+download (pull, since no group manifest RPC exists);
+ color palette sync is push-only via add_palette_colors (no read-back RPC exists); CheckConnection
+ = signed-in + membership check; StartMonitoring/StopMonitoring wire a new CloudCollectionMonitor
+ instance whose polled deltas raise the same low-level events FolderTeamCollection's file watcher
+ raises. Also wrote `Cloud/CloudCollectionMonitor.cs` (60s Timer polling get_changes, PollNow() for
+ on-activation/manual triggering, self-echo suppression falls out of the shared last_seen_event_id
+ cursor). Builds clean (`dotnet build src/BloomExe/BloomExe.csproj`).
+ Contract gaps/ambiguities found while implementing (see final report for full list): (1)
+ checkin-start/finish never return the server-assigned book id for a brand-new book -- worked
+ around with a post-commit get_collection_state refresh matched on bookInstanceId; (2) no manifest
+ RPC exists for collection-file groups (only a version-bump counter) -- worked around with a direct
+ S3 ListObjectsV2, reading latest rather than a pinned version; (3) add_palette_colors has no
+ matching read-back RPC -- palette sync is currently push-only; (4) member first/last name isn't in
+ the book row shape -- lock display fields lockedByFirstName/Surname are null for cloud TCs; (5)
+ collection-files-start/finish's exact response shape isn't spelled out -- assumed
+ checkin-start-like `{transactionId, s3}` / `{version}`; (6) the `members` RPC names
+ (members_list/add/remove/set_role) are a guess at CONTRACTS.md's "members: list/add/remove/
+ set_role" shorthand.
+ Base-class change identified but NOT made (file discission): `Bloom.History.BookHistoryEventType`
+ has no `WorkPreservedLocally` member; used the literal `100` per the task brief instead of adding
+ it to that shared enum (out of this task's owned-file scope) -- recommend the orchestrator add
+ `WorkPreservedLocally = 100` there.
+ Next action: write `Cloud/CloudJoinFlow.cs`, then the one authorized
+ TeamCollectionManager.cs edit, then tests.
+- 7 Jul 2026 · done · Wrote `Cloud/CloudJoinFlow.cs`: `ListMyCollections()` (my_collections),
+ `DetermineScenario()` (the six-scenario local-vs-remote matrix ported from
+ `FolderTeamCollection.ShowJoinCollectionTeamDialog`'s boolean logic: FreshJoin/
+ AlreadyJoinedSameCollection/LinkedToDifferentCloudCollection/LinkedToFolderTeamCollection/
+ PlainCollectionSameGuid/PlainCollectionDifferentGuid), `JoinCollection()` (creates the local
+ folder + link file + CloudTeamCollection + first Receive of collection files and every book;
+ throws `CloudJoinConflictException` for the three human-decision scenarios), and
+ `CreateAndJoinCollection()` (create_collection + join). Key design point: a cloud collection's
+ `.bloomCollection` file travels with it in the "other" collection-file group (same as a folder
+ TC's zipped settings), so joining does not need to fabricate a fresh CollectionSettings --
+ downloading collection files brings the real settings file down. Builds clean.
+ Scope note: the interactive resolution dialog for the three conflict scenarios (the cloud
+ equivalent of ShowJoinCollectionTeamDialog's React dialog) is UI-layer work outside this task's
+ file ownership; CloudJoinConflictException.Message is ready for a future dialog to display.
+ Next action: the one authorized TeamCollectionManager.cs edit (CreateTeamCollectionFromLink's
+ cloud branch + ConnectToCloudCollection), then the test suite (CloudTeamCollectionMemberTests,
+ CloudTeamCollectionLockTests, CloudSyncAtStartupTests, CloudCollectionMonitorTests), then
+ build/test verification and the final report.
+- 7 Jul 2026 · done · Made the one authorized TeamCollectionManager.cs edit: replaced both
+ NotImplementedException placeholders (`CreateTeamCollectionFromLink`'s cloud branch now calls a
+ new private `CreateCloudTeamCollection` helper mirroring `CreateFolderTeamCollection`;
+ `ConnectToCloudCollection(collectionId)` now creates the server-side row via create_collection
+ -- collectionId doubles as both this Bloom collection's own CollectionId GUID and the server's
+ `collections.id`, per CONTRACTS.md's link-format note -- links the local collection, and pushes
+ existing local content up via a new `CloudTeamCollection.SetupCloudTeamCollectionWithProgressDialog`
+ mirroring FolderTeamCollection's own Setup*WithProgressDialog). Touched nothing else in the file
+ (verified via `git diff --stat` + full diff review). Builds clean.
+ Next action: write the test suite (CloudTeamCollectionMemberTests, CloudTeamCollectionLockTests,
+ CloudSyncAtStartupTests, CloudCollectionMonitorTests) under src/BloomTests/TeamCollection/Cloud/,
+ run the cloud + TC-regression filters, then write the final report.
+- 7 Jul 2026 · done · Wrote the four acceptance-criteria test files under
+ src/BloomTests/TeamCollection/Cloud/ (CloudTeamCollectionMemberTests, CloudTeamCollectionLockTests,
+ CloudSyncAtStartupTests, CloudCollectionMonitorTests), reusing the FakeRestExecutor/
+ StubCloudAuthProvider/FakeResponses harness already established in CloudCollectionClientTests.cs
+ (same namespace, no duplication). `dotnet test src/BloomTests/BloomTests.csproj --filter
+ "FullyQualifiedName~Cloud"` → 102 passed, 0 failed. `--filter "FullyQualifiedName~TeamCollection"`
+ (folder-TC regression + all cloud) → 300 passed, 0 failed.
+ CloudSyncAtStartupTests ports the single highest-value scenario from the folder suite (checked-out
+ + locally-edited book whose repo checksum ALSO changed remotely -> SyncAtStartup's
+ PutBook(inLostAndFound: true) path) and asserts all three unified-recovery effects: a `.bloomSource`
+ file appears in a local "Lost and Found" folder, a `log_event` RPC fires, and a
+ "TeamCollection.Cloud.WorkPreservedLocally" message lands in the log; plus a simpler "new book
+ fetched from repo" case. Porting the full ~15-case SyncAtStartupTests.cs matrix is flagged as
+ follow-up work in the final report (needs a fuller scripted-server fake for rename/id-conflict
+ detection via GetRepoBookFile).
+ Real bug/behavior found while writing CloudTeamCollectionLockTests: `TeamCollection.AttemptLock`
+ (base class, unchanged) discards `TryLockInRepo`'s bool return value and returns based on the LOCAL
+ status it optimistically set to "locked by me" BEFORE calling TryLockInRepo -- it never re-reads
+ status afterward. So AttemptLock's own return value can be `true` even when the cloud server denied
+ the lock; only a separate `WhoHasBookLocked`/`GetStatus` call afterward reliably reflects who won
+ (which TryLockInRepo's own doc comment already anticipates: "the caller should re-read status to
+ find out who won"). Not fixed (TeamCollection.cs is read-only for this task) -- flagged in the
+ final report as a base-class change worth considering.
+ Next action: build/test verification is done; write the final report.
+- 7 Jul 2026 · done · Added `Cloud/CloudTeamCollectionLiveTests.cs` ([Explicit] Send-then-Receive
+ round trip against the REAL local dev stack: `supabase start` + `supabase functions serve
+ --env-file server/dev/functions.env`, started in the background per the anti-hang rules) and ran
+ it to green. It found and fixed TWO real bugs that no mocked test could have caught:
+ (1) **JSON serialization bug** in `CloudCollectionClient.CallRpc`/`CallEdgeFunction`: RestSharp's
+ own default JSON serializer doesn't know how to serialize a `Newtonsoft.Json.Linq.JArray`/
+ `JObject` embedded in the anonymous body object (it reflects over JToken's own CLR properties
+ instead of writing a native array/object), which silently produced a malformed `files` array and
+ a cryptic Postgres "jsonb_to_recordset must be an array of objects" error on the very first live
+ checkin-start call. Fixed by serializing the body with Newtonsoft ourselves and attaching the
+ resulting JSON text as a raw request-body parameter (`CloudCollectionClient.AddJsonBody` private
+ helper), rather than handing RestSharp a raw object to serialize itself.
+ (2) **Missing book-instance path segment on Receive**: `download-start`'s credentials are scoped
+ to the whole COLLECTION prefix (`tc/{cid}/`), not the book-specific one (`tc/{cid}/books/
+ {bookInstanceId}/`) that `checkin-start` returns for uploads -- `FetchBookFromRepo`/
+ `GetRepoBookFile` were building S3 keys directly under the collection prefix, producing
+ `NoSuchVersion` (confirmed via a manual Node/aws-sdk repro against the live MinIO). Fixed with a
+ new `CloudTeamCollection.BuildBookS3Location` helper that inserts `books/{bookInstanceId}/`
+ (looked up from the cache) before combining with `download-start`'s prefix.
+ Also confirmed a THIRD issue that is NOT fixable in this task's files: the live server currently
+ stamps `locked_by`/`created_by` with the raw auth user id (JWT `sub`, confirmed by decoding a
+ real session token) rather than the email CONTRACTS.md's identity model specifies ("account email
+ is the identity in cloud TCs") -- a task-01 SQL-function issue, out of scope here. Added a
+ client-side `ResolveLockedByForDisplay` workaround that resolves OUR OWN id back to our own email
+ (fixes every IsCheckedOutHereBy/"is this checked out to me" check, the case that matters most),
+ leaving a teammate's id unresolved to a friendly name pending the server-side fix.
+ Re-ran `--filter "FullyQualifiedName~TeamCollection&FullyQualifiedName!~LiveTests"` after all
+ three fixes: still 300 passed, 0 failed. Live test: 1 passed (run manually with
+ `BLOOM_CLOUDTC_ANON_KEY` exported, `supabase functions serve --env-file server/dev/functions.env`
+ running in the background).
+ Next action: none for this task; final report follows. Recommend the orchestrator route the
+ locked_by-is-a-uuid finding to whoever owns task 01's SQL functions.
diff --git a/Design/CloudTeamCollections/tasks/06-api-endpoints.md b/Design/CloudTeamCollections/tasks/06-api-endpoints.md
new file mode 100644
index 000000000000..5933a244c85d
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/06-api-endpoints.md
@@ -0,0 +1,43 @@
+# 06 — API endpoints (Wave 3, after 05)
+
+**Goal**: expose cloud operations to the browser UI.
+
+**Dependencies**: 05. **Exclusive owner of shared file `TeamCollectionApi.cs`** during this
+task. Owns new `src/BloomExe/web/controllers/SharingApi.cs`.
+
+## Steps
+- [x] `SharingApi`: `sharing/members` (GET), `sharing/addApproval`, `sharing/removeApproval`,
+ `sharing/setRole`, `sharing/loginState`, `sharing/login`, `sharing/logout`,
+ `collections/mine` (Get-my-Team-Collections), `collections/pullDown`. Plus (matching what
+ the Wave-2/UI tasks already call): `sharing/showSignIn`, `sharing/forceUnlock`,
+ `sharing/history`, `sharing/historyCache`.
+- [x] `TeamCollectionApi` additions (existing endpoints untouched for folder TCs): book-status
+ JSON gains `localVersionSeq`/`repoVersionSeq`/`signedIn`/capability flags (additive);
+ `teamCollection/receiveUpdates`; `sendAll` alias of checkInAllBooks for cloud; force-unlock
+ routes through the audited RPC. Plus `teamCollection/capabilities`, `tcStatusMetadata`,
+ `cloudCollectionId`, `isUserAdmin`, `createCloudTeamCollection`,
+ `showCreateCloudTeamCollectionDialog` (all named by the Wave-2 UI tasks' mocks).
+- [x] Websocket pushes for member-list changes and status refresh reuse existing contexts
+ (`BloomWebSocketServer.SendEvent`, same as folder TCs already use).
+- [x] One authorized migration: `20260707000006_tc_locked_by_display.sql`
+ (`lockedByEmail`/`lockedByName` on get_collection_state/get_changes/get_book_manifest).
+
+## Acceptance
+- `SharingApiTests` + `TeamCollectionApiTests` additions (permissions, listing, status fields).
+ DONE: `SharingApiTests.cs` (9 tests) + `TeamCollectionApiCloudTests.cs` (10 tests).
+- Folder-TC API behavior byte-identical (existing tests untouched and green). DONE: full
+ `~TeamCollection` suite 311/311; `AddCloudBookStatusFields` returns the exact same string
+ instance (not even a re-parse) when the current collection isn't a `CloudTeamCollection`.
+
+**Agent notes**: Sonnet. Thin pass-throughs to `CloudCollectionClient`; no business logic here.
+
+## Progress log
+- 7 Jul 2026 · done: full task — migration (live-verified against the local dev stack), all
+ SharingApi + TeamCollectionApi endpoints, live-verification fixes to
+ `CloudCollectionClient.MembersRemove/MembersSetRole` (wrong RPC param name — found by actually
+ calling the deployed RPCs) and a `CloudTeamCollection` auth-initialization gap (reopening an
+ existing cloud TC never called `InitializeAtStartup`), tests (112/112 Cloud, 31/31
+ TeamCollectionApi, 311/311 full TeamCollection). See the task's final report (orchestrator
+ merge notes) for the full endpoint list, known gaps (pullDown's untested manager=null path,
+ no dedicated sign-in dialog yet, WireUpForWinforms bundle-collision finding for the UI team),
+ and exact next action (UI wiring, task 07/08's "next" notes).
diff --git a/Design/CloudTeamCollections/tasks/07-ui-setup.md b/Design/CloudTeamCollections/tasks/07-ui-setup.md
new file mode 100644
index 000000000000..0c1a7dbdc9f5
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/07-ui-setup.md
@@ -0,0 +1,299 @@
+# 07 — UI: setup, settings, sharing panel, chooser (Wave 1 shells → Wave 3 wiring)
+
+**Goal**: the create/share/join surfaces, Notion-simple.
+
+**Dependencies**: shells against mocked endpoints in Wave 1; real wiring after 06.
+Owns new `src/BloomBrowserUI/teamCollection/SharingPanel.tsx`,
+`JoinCloudCollectionDialog.tsx`; **exclusive owner of** `CreateTeamCollection.tsx`,
+`TeamCollectionSettingsPanel.tsx`, `CollectionChooserDialog` during its waves.
+
+## Steps
+- [x] Settings (not shared): keep folder-TC button; add "Share this collection on the Bloom
+ sharing server (experimental)" behind the experimental flag + feature gate, disabled
+ state explains gating.
+- [x] Cloud create dialog: sign-in step (inline; in dev auth mode this is a plain
+ email/password form driven by `sharing/loginState`'s reported mode — the real
+ BloomLibrary browser flow slots in later), immutable-name acknowledgement, initial Send
+ progress; no folder chooser, no Dropbox checkboxes, no restart.
+- [x] SharingPanel (cloud TCs): approved-emails list (avatar, name-when-claimed, email, role
+ chip, claimed/pending), add-with-role, remove (warns: force-unlocks their checkouts),
+ change role; last-admin protections; member read-only view. Folder TCs keep old panel.
+- [x] Collection chooser: "Get my Team Collections" (signed-out state included); pull-down join
+ via the six-scenario dialog (new states: NotSignedIn, ApprovalRemoved).
+- [x] Registration dialog: email unlock for cloud TCs (identity = account).
+- [x] All strings via XLF (DistFiles/localization/en only), Send/Receive terminology.
+
+## Acceptance
+- vitest browser-mode component tests: SharingPanel CRUD/pending/last-admin/read-only;
+ chooser listing + signed-out; create dialog gating and flow.
+- `yarn lint` clean. (Never run `yarn build`.)
+
+**Agent notes**: Sonnet. Emotion `css` prop styling; arrow-function components; no prop
+destructuring — follow src/BloomBrowserUI/AGENTS.md.
+
+## Progress log
+- 2026-07-06 · done: `sharingApi.ts` (shells for the Wave-3 SharingApi endpoints) and
+ `SharingPanel.tsx`/`SharingMembersList` (approved-accounts CRUD, claimed/pending, last-admin
+ protection, member read-only view) with `SharingPanel.test.tsx` (7 tests, all green); also
+ fixed a pre-existing vitest gap (`processSimpleMarkdown` missing from the
+ `localizationManager` mock, which crashed any test rendering `Div`/`BloomButton`/etc.) ·
+ next: wire the "Share this collection..." experimental-gated entry point into
+ TeamCollectionSettingsPanel.tsx (step 1).
+- 2026-07-06 · done: resumed session; re-verified WIP files are prettier-clean (no changes)
+ and `SharingPanel.test.tsx` (7 tests) still passes — `yarn vitest run` in default `forks`
+ pool hit "Timeout starting forks runner" on this machine, so re-ran with `--pool=threads`,
+ which passed cleanly in 37.8s; will use `--pool=threads` for the rest of this session.
+- 2026-07-06 · done: step 1 (Settings). Added
+ `useIsCloudTeamCollectionsExperimentalFeatureEnabled()` to `sharingApi.ts` (reads
+ `app/enabledExperimentalFeatures`, checks for the `cloud-team-collections` token — matches
+ `ExperimentalFeatures.kCloudTeamCollections` in C#) and a new "Share this collection on the
+ Bloom sharing server (experimental)" `BloomButton` in `TeamCollectionSettingsPanel.tsx`'s
+ not-yet-a-TC branch, disabled until the experimental flag is on, with
+ `l10nTipEnglishDisabled` explaining the gate; posts
+ `teamCollection/showCreateCloudTeamCollectionDialog` (new Wave-3 endpoint) when enabled.
+ Also added `createCloudTeamCollection()` to `sharingApi.ts` for the next step · next: cloud
+ create dialog (sign-in → immutable-name ack → initial Send progress) in
+ `CreateTeamCollection.tsx` (step 2).
+- 2026-07-06 · done: step 2 (Cloud create dialog). Added `CreateCloudTeamCollectionBody`
+ (presentational, four states derived from props: sign-in [dev-mode email/password form or a
+ cloud-mode "Sign in with your Bloom account" placeholder button per `loginState.mode`] →
+ immutable-name-acknowledgement checkbox gating the Share button → sending [LinearProgress] →
+ done/error) and `CreateCloudTeamCollectionDialog` (container wiring it to
+ `useSharingLoginState`/`signIn`/`createCloudTeamCollection` from `sharingApi.ts` and the
+ BloomDialog frame; no folder chooser, no Dropbox checkboxes; bottom button is Cancel until
+ done, then Close — no restart) in `CreateTeamCollection.tsx`. Fixed `createCloudTeamCollection`
+ to use `postJson` instead of `post` (the latter is fire-and-forget and returns no promise —
+ would have crashed the `.then()` chain). Added `CreateCloudTeamCollection.test.tsx` (10
+ tests, all green, same raw-DOM-render pattern as `SharingPanel.test.tsx`) covering sign-in
+ gating (dev vs cloud mode), email/password field wiring, name-ack gating of the Share button,
+ sending/error/done states. `yarn eslint` on all touched files: 0 errors, pre-existing warnings
+ only (unrelated lines in the original `CreateTeamCollectionDialog`) · next: Collection chooser
+ "Get my Team Collections" (signed-in listing + signed-out state) in `CollectionChooser.tsx`
+ (step 3), then `JoinCloudCollectionDialog.tsx` (new, six-scenario + NotSignedIn +
+ ApprovalRemoved) for pull-down join.
+- 2026-07-06 · done (partial step 3 — listing half; pull-down-join dialog still to do): added
+ `MyCloudCollectionsSection.tsx` (new; presentational "Get my Team Collections" sidebar of the
+ collection chooser: signed-out sign-in prompt, loading, empty state, and a listing with a
+ per-row pull-down button) and wired it into `CollectionChooser.tsx` alongside the existing
+ recent-collections grid, backed by `useSharingLoginState`/`useMyCloudCollections`/
+ `pullDownCollection` from `sharingApi.ts`. Added `MyCloudCollectionsSection.test.tsx` (4
+ tests, all green) covering signed-out/loading/empty/listing+pull-down-click. Found and fixed
+ the same "l10n `Div`/`P`/`Span` don't forward `data-testid`" trap as elsewhere in this task —
+ wrapped in a plain `
` for the loading/empty states. `yarn eslint` clean
+ on all touched files · next: `JoinCloudCollectionDialog.tsx` (new) — the pull-down-join dialog
+ with the six folder-TC scenarios adapted to cloud collections plus NotSignedIn and
+ ApprovalRemoved (8 states total), to complete step 3; `onPullDown` in `CollectionChooser.tsx`
+ will eventually need to open it once it exists (Wave-1 shell can leave the direct
+ `pullDownCollection` call as the placeholder action for now).
+- 2026-07-06 · done (step 3 complete): added `JoinCloudCollectionDialog.tsx` (new) — the
+ pull-down-join dialog, structurally mirroring the folder-TC `JoinTeamCollectionDialog.tsx`'s
+ six scenarios (CreateNewCollection, MatchesExistingNonTeamCollection,
+ MatchesExistingTeamCollection[Elsewhere], MatchesDifferentTeamCollection,
+ IncompleteLocalCopy — renamed from IncompleteTeamCollection since the cloud failure mode is a
+ corrupt local pull-down cache, not a missing ".txt" file) plus two new states unique to cloud
+ collections: NotSignedIn (action button becomes "Sign In", posts `sharing/showSignIn`) and
+ ApprovalRemoved (action button disabled, explains the user isn't on the approved list). The
+ join/pull-down action calls `pullDownCollection` from `sharingApi.ts`. Added
+ `JoinCloudCollectionDialog.test.tsx` (8 tests, all green) covering all eight states; had to
+ query `document.body` rather than the local render container since MUI's `Dialog` portals its
+ content, and to assert on l10n *keys* rather than English text because the test-only
+ `localizationManager` mock resolves every key to itself (same trap noted in
+ `SharingPanel.test.tsx`). Not wired into `CollectionChooser.tsx`'s `onPullDown` yet — knowing
+ which of the eight states applies requires the six-scenario matching logic that task
+ 05-cloud-backend.md says lives server/backend-side and doesn't exist until later; the chooser
+ keeps calling `pullDownCollection` directly for now (Wave-1 shell). Similarly, `SharingPanel`
+ is not wired into `TeamCollectionSettingsPanel.tsx`'s `isTeamCollection` branch yet — that
+ needs a "is this a cloud TC" signal that isn't available until the backend capability flags
+ land; both are complete, tested, standalone shells ready for that wiring. `yarn eslint`
+ clean on all touched files · next: Registration dialog email unlock for cloud TCs (step 4),
+ then XLF strings for everything added in this task (step 5, follow
+ `.github/skills/xlf-strings/SKILL.md`, only `DistFiles/localization/en/`).
+- 2026-07-06 · done: step 4 (Registration dialog). Added `cloudAccountEmail?: string` to
+ `IRegistrationContentsProps` (registrationTypes.ts) and `IRegistrationDialogProps`
+ (registrationDialogLauncher.tsx), threaded through `RegistrationDialog` and both launchers
+ (`RegistrationDialogLauncher`, `RegistrationDialogEventLauncher`). In
+ `registrationContents.tsx`, when `cloudAccountEmail` is set: the email field is force-synced
+ to it and always locked (`mayChangeEmail` becomes `false` regardless of the prop — identity is
+ the signed-in account, not a free-text field), with a distinct label
+ ("RegisterDialog.CloudAccountEmail") and an explanatory note
+ ("RegisterDialog.CloudAccountEmailNote") instead of the folder-TC "Check in to change email"
+ message (which means something different — "already registered" rather than
+ "tied to your account"). Wired this into `CreateCloudTeamCollectionDialog` in
+ `CreateTeamCollection.tsx`: `startSend` now checks `registration/userInfo` first (same pattern
+ as the folder dialog's `tryToCreate`) and shows the registration dialog with
+ `cloudAccountEmail: loginState.email` if not yet registered, only calling the actual
+ `createCloudTeamCollection()` (renamed the old body to `doSend`) once registered. No new test
+ file added (no pre-existing vitest coverage for the registration components in this repo —
+ only Playwright `.uitest.ts` files, which are excluded from the vitest run and out of scope
+ here); re-ran the full task test suite (29 tests across
+ SharingPanel/CreateCloudTeamCollection/MyCloudCollectionsSection/JoinCloudCollectionDialog) to
+ confirm no regressions. `yarn eslint` clean on all touched files · next: step 5 — XLF strings
+ for every new user-visible string added across this whole task (follow
+ `.github/skills/xlf-strings/SKILL.md`; only `DistFiles/localization/en/`). This is the last
+ step before final report.
+- 2026-07-07 · done: step 5 (XLF strings) was already completed in the prior WIP commit
+ (`8c82b4b37`) but not yet logged/verified — closing out the task. Confirmed every new
+ user-facing string across `CollectionChooser`/`MyCloudCollectionsSection`, `SharingPanel`,
+ `CreateTeamCollection`'s cloud dialog, `JoinCloudCollectionDialog`, and the registration
+ cloud-account additions goes through `l10nKey`/`Div`/`P`/`Span`/`BloomButton`/
+ `AttentionTextField` (no hardcoded UI text), and that every one of those keys has a matching
+ `translate="no"` entry with an `ID:` note in `DistFiles/localization/en/Bloom.xlf` or
+ `BloomMediumPriority.xlf` (priority split follows the SKILL.md table: primary actions/labels
+ in `Bloom.xlf`, secondary/help/error text in `BloomMediumPriority.xlf`), each with a second
+ translator-context note wherever the string is short/generic, a fragment, or contains a
+ placeholder or product name — including the auto-derived
+ `TeamCollection.Sharing.ShareOnCloudServer.ToolTipWhenDisabled` key consumed via
+ `l10nTipEnglishDisabled` on the Settings button. No existing entries were modified. Only
+ `DistFiles/localization/en/` was touched. Terminology check: the initial collection upload
+ uses "Send"/"Sending" (matching folder-TC Send/Receive language), and the join/pull-down flow
+ uses "Get"/"download" — consistent with the existing folder-TC `JoinTeamCollectionDialog`'s
+ own wording, so no new/conflicting terminology was introduced.
+- 2026-07-07 · done: final wrap-up for task 07. Ran `yarn prettier --write` on every file this
+ branch touched (`git diff --name-only 087e9a725..HEAD`); all 16 already matched (the two WIP
+ commits that skipped the pre-commit hook turned out to be prettier-clean already, so no
+ reformatting was needed). Re-ran the full component-test suite in single-run mode:
+ `yarn vitest run --pool=threads teamCollection/SharingPanel.test.tsx
+ teamCollection/CreateCloudTeamCollection.test.tsx
+ collection/MyCloudCollectionsSection.test.tsx teamCollection/JoinCloudCollectionDialog.test.tsx`
+ → 4 test files, 29 tests, all passing. Ran `yarn lint` across the whole project: 0 errors,
+ 777 warnings, none of them on lines this branch added or in files newly created by this
+ branch (checked `CreateTeamCollection.tsx`'s two reported warnings and `vitest.setup.ts`'s
+ one — both sit on pre-existing lines outside this branch's diff hunks). All five task steps
+ are now checked off. Task 07 (Wave-1 shells) is complete and ready for Wave-3 wiring per the
+ "next" notes left in each shell (SharingPanel into `TeamCollectionSettingsPanel`'s
+ `isTeamCollection` branch; `JoinCloudCollectionDialog`'s eight-state matching logic into
+ `CollectionChooser`'s `onPullDown`) once the relevant backend capability flags/matching logic
+ from tasks 05/06 land — see the note at the end of the 2026-07-06 step-3 entry above.
+
+## Wave-3 UI wiring (task/ui-wiring branch, resumable per orchestration/ui-wiring.prompt.md)
+
+- 2026-07-07 · done: item 1 (the live folder-TC breakage) + item 2 (dedicated sign-in dialog).
+ Root cause confirmed: `createTeamCollectionDialogBundle` is ONE shared Vite entry
+ (`CreateTeamCollection.tsx`) that used to have both `CreateTeamCollectionDialog` (folder) and
+ `CreateCloudTeamCollectionDialog` (cloud) call `WireUpForWinforms` at module scope — that
+ function sets a single global (`window.wireUpRootComponentFromWinforms`), so whichever call
+ ran last at module load silently won, breaking the other dialog (in practice: the folder-TC
+ "Create Team Collection" dialog could no longer open, since the cloud dialog's call always
+ ran second in source order). Fixed by introducing `CreateTeamCollectionBundleDispatcher`
+ (new, in `CreateTeamCollection.tsx`) as the ONLY component in the file that calls
+ `WireUpForWinforms`; it renders one of `CreateTeamCollectionDialog` /
+ `CreateCloudTeamCollectionDialog` / the new `SignInDialog` based on a `dialogKind` prop
+ ("folder" / "cloud" / "signIn") that C# now always passes. Updated all three C# call sites to
+ pass it: `TeamCollectionApi.cs`'s `HandleShowCreateTeamCollectionDialog` (`"folder"`) and
+ `HandleShowCreateCloudTeamCollectionDialog` (`"cloud"`), and `SharingApi.cs`'s
+ `HandleShowSignIn` (`"signIn"`, also shrunk its dialog size from 600x580 to 420x320 to fit
+ the much smaller sign-in-only form). Also fixed the parallel Vite-dev-mode entry
+ (`CreateTeamCollection.entry.tsx`), which bootstrapped `CreateTeamCollectionDialog` by name
+ but — since it's the same module — was *already* silently rendering whichever dialog's
+ `WireUpForWinforms` call happened to win, the same bug in a different guise; it now
+ bootstraps the dispatcher too. New `SignInDialog.tsx` (item 2): a small dedicated sign-in
+ dialog (dev-mode email/password form per `loginState.mode`; a "Signing in ... isn't available
+ yet" message for the eventual production/"cloud" mode), replacing the old placeholder
+ behavior where `sharing/showSignIn` reused the cloud create-collection dialog's first screen
+ even in contexts unrelated to creating a collection. Auto-closes once `useSharingLoginState`
+ reports `signedIn: true` (picks up the "sharing"/"loginState" websocket event
+ `SharingApi.HandleLogin` already raises). Reuses three existing XLF keys
+ (`TeamCollection.Sharing.EmailAddress`/`Password`/`SignIn`) and adds one new one,
+ `TeamCollection.Sharing.SignInNotYetAvailable`, to `BloomMediumPriority.xlf` (secondary/error
+ text per the skill's priority table). New tests: `SignInDialog.test.tsx` (4 tests: dev-mode
+ form, dev-mode Sign In click, dev-mode error display, cloud-mode not-yet-available message —
+ the presentational `SignInDialogBody` only, same pattern as `CreateCloudTeamCollectionBody`'s
+ own tests) and `CreateTeamCollectionBundleDispatcher.test.tsx` (4 tests: a direct regression
+ test for the bug, proving the dispatcher renders the right component for every `dialogKind`
+ including the omitted/default case, which must still be the folder dialog). Full run:
+ `yarn vitest run --pool=threads teamCollection/CreateTeamCollectionBundleDispatcher.test.tsx
+ teamCollection/SignInDialog.test.tsx teamCollection/CreateCloudTeamCollection.test.tsx
+ teamCollection/SharingPanel.test.tsx teamCollection/JoinCloudCollectionDialog.test.tsx` → 5
+ files, 33 tests, all green (no regressions in the two pre-existing suites this change's
+ neighbors touch). `yarn eslint` on all touched/new files: 0 errors, 3 pre-existing warnings
+ (all on lines predating this change, in the untouched `CreateTeamCollectionDialog` body).
+ `dotnet build src/BloomExe/BloomExe.csproj`: succeeds, 0 errors (needed `./init.sh` first in
+ this fresh worktree — missing `PodcastUtilities`/`IDevice` per AGENTS.md's known-issue note).
+ Note for future sessions in this sandbox: `yarn vitest`/`yarn eslint`/`dotnet build` need
+ `dangerouslyDisableSandbox: true` on the Bash call — the default sandbox blocks
+ worker-thread/child-process spawning, which otherwise fails every vitest pool
+ ("Timeout starting threads runner") within ~5s. · next: item 3 (wire `SharingPanel` into
+ `TeamCollectionSettingsPanel`'s `isTeamCollection` branch for cloud TCs).
+- 2026-07-07 · done: item 3. `TeamCollectionSettingsPanel.tsx`'s `isTeamCollection` branch now
+ renders `SharingPanel` (collectionId from `useCloudCollectionId()`, currentUserEmail from
+ `useSharingLoginState().email`, isAdmin from `useIsTeamCollectionAdmin()`) when
+ `isCloudTeamCollection(useTeamCollectionCapabilities())` is true; folder TCs keep the old
+ free-text administrator-emails field + "Cloud Storage Folder Location" link completely
+ unchanged (the link/`fileIO/showInFolder` call doesn't make sense for a cloud TC's
+ `cloud://sil.bloom/collection/{id}` `RepoDescription`, confirmed by reading
+ `CloudTeamCollection.RepoDescription` in `TeamCollection/Cloud/CloudTeamCollection.cs`). All
+ four hooks used are already gated internally on the experimental feature, so this adds zero
+ extra requests for folder TCs. New `TeamCollectionSettingsPanel.test.tsx` (3 tests: folder TC
+ shows the old field not SharingPanel; cloud TC shows SharingPanel wired to the real
+ collectionId/email/isAdmin values not the old field; not-yet-a-TC shows neither) — mocks
+ `teamCollectionApi`/`sharingApi`/`SharingPanel` itself (already covered by its own
+ `SharingPanel.test.tsx`) so this file only tests the branching logic this task adds.
+- 2026-07-07 · done: item 4. `JoinCloudCollectionDialog`'s `handleJoinClick` now actually wires
+ `pullDownCollection`'s promise (previously fired-and-forgotten): success closes the dialog and
+ calls a new optional `onClose` prop; failure shows the server's real error message in a new
+ `ErrorBox` and re-enables the action button for retry. Added the `onClose` prop and removed
+ the dialog's own (unused, and actively dangerous once embedded — see below) `WireUpForWinforms`
+ call. `CollectionChooser.tsx`'s `onPullDown` now looks up the clicked row's full
+ `ICloudCollectionSummary` from the already-fetched `cloudCollections` list and renders
+ `JoinCloudCollectionDialog` inline (embedded directly in the tree, not as a separate WinForms
+ dialog — there never was a C# call site or bundle entry for one), passing the real
+ `signedIn`/`collectionId`/`collectionName`. Documented, deliberate scope limit: no endpoint
+ exposes the six local-vs-remote matching flags (`existingCollection` etc.) ahead of an actual
+ pull-down attempt — `CloudJoinFlow.DetermineScenario`/`JoinCollection` (task 05) resolve that
+ server-side, inside `collections/pullDown` itself, and `SharingApi.HandlePullDown` only
+ surfaces a human-readable message on conflict (via `CloudJoinConflictException.Message`), not
+ the structured `JoinScenario` enum/`LocalCollectionFolder` it also carries. So the dialog
+ defaults to the ordinary `CreateNewCollection` copy and, if a real conflict is hit, shows the
+ server's real message instead of guessing which of the eight dialog states to switch to (a
+ future task could plumb `JoinScenario`/`LocalCollectionFolder` through the failure response to
+ make this exact, but that's new backend surface, out of this wiring task's scope). Found (and
+ fixed as part of this item, since it would otherwise become live once `JoinCloudCollectionDialog`
+ is imported into `CollectionChooser`'s bundle) a second instance of item 1's bug class: the
+ dialog's leftover `WireUpForWinforms(JoinCloudCollectionDialog)` call (dead code — no C# call
+ site/bundle entry ever pointed at it) would have silently overwritten
+ `CollectionChooserDialog`'s own `WireUpForWinforms` registration in the shared
+ `collectionChooserBundle` the moment this task's import wired them into the same module graph.
+ New tests: 2 added to `JoinCloudCollectionDialog.test.tsx` (closes + calls `onClose` on
+ success; shows the server's real error and stays open, button re-enabled, on failure — updated
+ the file's `pullDownCollection` mock to return a resolved promise by default, since the
+ existing 8 state tests all synchronously assert `pullDownCollection` was called without
+ needing to await it) and new `CollectionChooser.test.tsx` (2 tests: pull-down opens the dialog
+ for the exact row clicked with real login/collection data, and `onClose` removes it; dialog
+ never renders when the cloud feature is off) — mocks `sharingApi`/`JoinCloudCollectionDialog`
+ itself.
+- 2026-07-07 · done: item 5 (sweep for lingering mock-only defaults). No behavioral defaults
+ needed changing: every `get()` call in `sharingApi.ts`/`teamCollectionApi.tsx` already omits an
+ error callback, so a real endpoint failure throws (unhandled rejection, reported to
+ Sentry/console) rather than being silently swallowed into some "safe" default — already
+ fail-fast per AGENTS.md, and consistent with every other hook in this file family. The `?? []`
+ /`?? initialTeamCollectionCapabilities`-style fallbacks that exist are all inside the SUCCESS
+ callback (a 200 response with an unexpectedly-empty body), not the failure path, so they don't
+ mask anything. What WAS stale: doc comments in `sharingApi.ts` (top-of-file), `teamCollectionApi.tsx`
+ (the Cloud Team Collections section header), `CreateTeamCollection.tsx` (the cloud-dialog
+ section banner), and `TeamCollectionButton.test.tsx` claiming the relevant endpoints were
+ "not-yet-implemented" / "mocked" / task-06-pending — all now real per task 06's merge. Left as
+ a misleading residue, this is exactly the kind of thing that could make a future
+ agent/developer distrust or re-mock already-real endpoints, or skip auditing their error paths
+ on the (false) assumption they're still shells. Updated all four to say what's actually true
+ now. No production code behavior changed by this item.
+- 2026-07-07 · done: item 6 (final sweep). `yarn vitest run --pool=threads teamCollection
+ collectionsTab collection react_components/TopBar`: this sandbox's vitest pool is slow enough
+ (each file's "collect" phase alone often 40-100s+) that the single combined invocation the
+ task brief names hung at final teardown past this session's tool timeout before printing its
+ summary line (no failures were reported by any individual file before that point, and no
+ hanging-process reporter was needed to diagnose it: `ps aux | grep vitest` showed the process
+ already gone, so it was killed externally, not deadlocked) — split into per-directory/file runs
+ instead to get a clean result within the timeout, then cross-checked file counts (`find
+ teamCollection -name "*.test.tsx"` etc.) against what each run actually reported to make sure
+ nothing silently dropped. Full accounting: `teamCollection` (11 files, 65 tests: ShareButton 4,
+ JoinCloudCollectionDialog 10, CollectionHistoryTable 5, CreateTeamCollectionBundleDispatcher 4,
+ TeamCollectionBookStatusPanel 11, SharingPanel 7, CreateCloudTeamCollection 10, SignInDialog 4,
+ TeamCollectionDialog 5, TeamCollectionSettingsPanel 3, NewerVersionAvailableMarker 2),
+ `collectionsTab` (0 test files -- nothing to run there), `collection` (2 files, 6 tests:
+ MyCloudCollectionsSection 4, CollectionChooser 2), `react_components/TopBar` (1 file, 8 tests:
+ TeamCollectionButton). **Total: 14 files, 79 tests, all green, zero failures.** `yarn lint`
+ (whole project, one process, no pool-timeout issue): 0 errors, 777 warnings -- the exact same
+ count task 07's own final wrap-up entry reported before this branch's work started, confirming
+ zero new warnings introduced across all six items. `dotnet build src/BloomExe/BloomExe.csproj`
+ re-verified clean (0 errors) after the last commit. All six work items from
+ `orchestration/ui-wiring.prompt.md` are now complete.
diff --git a/Design/CloudTeamCollections/tasks/08-ui-collection-tab.md b/Design/CloudTeamCollections/tasks/08-ui-collection-tab.md
new file mode 100644
index 000000000000..43b93981e072
--- /dev/null
+++ b/Design/CloudTeamCollections/tasks/08-ui-collection-tab.md
@@ -0,0 +1,210 @@
+# 08 — UI: collection tab (Wave 2 shells → Wave 3 wiring)
+
+**Goal**: status button, status/history dialog, Share button, per-book panel states.
+
+**Dependencies**: shells in Wave 2 (mocked); wiring after 06. **Exclusive owner of**
+`TeamCollectionButton.tsx`, `TeamCollectionDialog.tsx`, `TeamCollectionBookStatusPanel.tsx`,
+`statusPanelCommon.tsx`, `CollectionHistoryTable.tsx` during its waves.
+
+## Steps
+- [x] Status button: same chip/colors, driven by live metadata ("Updates available (3 books)").
+- [x] Status dialog: "Receive Updates" primary action (Reload remains only for applied
+ collection-settings changes); "Send All"; message log unchanged.
+- [x] Share button beside the status button → SharingPanel (admin manage / member read-only).
+- [x] Per-book panel: keep Check out/Check in + note field + avatars; add signedOut (with
+ Sign-in action), updatesAvailable, offline-disabled-with-reason states; check-in progress
+ = modal Send; "Force Unlock (Administrator Only)" wired to the audited RPC.
+- [x] Book thumbnails: holder-avatar overlay unchanged; subtle "newer version exists" marker.
+- [x] History tab: server events feed for cloud TCs (incl. incident entries), local cache for
+ offline; folder TCs unchanged.
+
+## Acceptance
+- Component tests: panel state matrix (incl. new states), status-button states, history
+ rendering incl. incidents.
+- `yarn lint` clean; folder-TC UI behavior unchanged.
+
+**Agent notes**: Sonnet. `StatusPanelState` additions must stay in sync with the C# status
+JSON (CONTRACTS.md, book-status section).
+
+## Progress log
+
+- 7 Jul 2026 · Status button done. Added `teamCollectionApi.tsx` shared plumbing for Wave 2:
+ `ITeamCollectionCapabilities`/`useTeamCollectionCapabilities` (mocked `teamCollection/capabilities`),
+ `isCloudTeamCollection()` helper (branch on capability, not concrete type),
+ `useTeamCollectionStatusMetadata` (mocked `teamCollection/tcStatusMetadata`),
+ `useCloudCollectionId`/`useIsTeamCollectionAdmin` (mocked, for the upcoming Share button), and
+ the additive `IBookTeamCollectionStatus` fields from CONTRACTS.md
+ (localVersionSeq/repoVersionSeq/signedIn/requiresSignIn/offlineDisabledReason). All these hooks
+ only call their endpoint when the cloud-team-collections experimental feature is on, so folder
+ Team Collections make zero extra requests and see zero UI change. `TeamCollectionButton.tsx` now
+ shows "Updates Available (N books)" when the metadata provides a count. Also front-loaded the
+ full Wave-2 XLF string set into `Bloom.xlf` (steps 1-4's strings) in this commit, since they were
+ designed together; later steps consume already-added ids rather than adding more XLF. New test:
+ `TeamCollectionButton.test.tsx` (8 tests, passing). `yarn eslint` clean (1 pre-existing warning
+ unrelated to this change, in `useTColBookStatus`'s dependency array).
+ Next action: implement step 2 (status dialog "Receive Updates" / "Send All"), then run its
+ tests + prettier + commit.
+- 7 Jul 2026 · Status dialog done (resumed after a session-limit interruption; predecessor's
+ in-flight `TeamCollectionDialog.tsx` change and new `TeamCollectionDialog.test.tsx` had been
+ preserved by the orchestrator in a WIP commit). `checkInAll`'s l10nKey/label switches to
+ `TeamCollection.SendAll`/"Send All" and its post target to `teamCollection/sendAllBooks` when
+ `isCloudTeamCollection(useTeamCollectionCapabilities())`; a new `receiveUpdates` button
+ (`TeamCollection.ReceiveUpdates`, posts `teamCollection/receiveUpdates`) appears beside the
+ existing "Reload Collection" button only for cloud TCs and only when `showReloadButton` is
+ false, so the two are mutually exclusive (Reload stays reserved for applied collection-settings
+ changes, per the design doc). Folder Team Collections are unaffected: `isCloud` is false
+ whenever the capabilities hook's mocked endpoint is never called (flag off) or reports no cloud
+ support, so `checkInAll` keeps its exact previous key/label/endpoint and `receiveUpdates` never
+ renders. Fixed up the predecessor's WIP test file: removed leftover debug `console.log`s, made
+ its `afterEach` use the repo's established `renderedContainer`/`unmountRoot` cleanup (matching
+ `SharingPanel.test.tsx`/`JoinCloudCollectionDialog.test.tsx`) instead of a bare
+ `document.body.innerHTML = ""`, and — the actual bug blocking all but one assertion — switched
+ button lookups from matching visible English text to matching by element `id`
+ (`checkInAll`/`receiveUpdates`/`reload`), because the vitest-only localizationManager mock
+ resolves every `l10nKey` to the key itself rather than the English fallback (same gotcha
+ documented in `JoinCloudCollectionDialog.test.tsx`'s file comment); the predecessor's
+ English-text lookups could never match and were failing 4 of 5 tests before this fix.
+ `TeamCollectionDialog.test.tsx`: 5 tests, all passing. `yarn eslint` clean on both changed
+ files. No new XLF entries needed — `TeamCollection.SendAll`/`TeamCollection.ReceiveUpdates`
+ were already front-loaded into `Bloom.xlf` in the step-1 commit.
+ Next action: implement step 3 (Share button beside the status button → SharingPanel).
+- 7 Jul 2026 · Share button done. New `teamCollection/ShareButton.tsx`: gated on
+ `isCloudTeamCollection(useTeamCollectionCapabilities())` — folder Team Collections render
+ nothing (not even a hidden node), so this is strictly additive UI. For cloud TCs, renders a
+ `TeamCollection.Sharing.ShareButton` ("Share") button next to `TeamCollectionButton` (wired
+ into `CollectionTopBarControls.tsx`, which isn't reserved by any other Wave-2 task per
+ IMPLEMENTATION.md's shared-file schedule); clicking it opens an MUI `Popover` anchored under
+ the button containing the existing `SharingPanel` (task 07), fed by
+ `useCloudCollectionId()`/`useIsTeamCollectionAdmin()`/`useSharingLoginState().email` — so an
+ admin gets the manage view and a regular member gets SharingPanel's own read-only view, with
+ no new branching needed here. No new XLF: `TeamCollection.Sharing.ShareButton` was already
+ front-loaded in the step-1 commit; used `@mui/icons-material/Share` (no dialog title needed,
+ so no new string). New test: `ShareButton.test.tsx` (4 tests: folder TC renders nothing;
+ cloud TC shows the button without opening the panel; admin click opens SharingPanel with
+ `isAdmin: true`; non-admin click opens it with `isAdmin: false` and the member's own email) —
+ SharingPanel itself is mocked (already unit-tested in `SharingPanel.test.tsx`) to a
+ prop-recording stub; MUI's Popover portals to `document.body` like MUI Dialog, so assertions
+ query `document` (same pattern as `JoinCloudCollectionDialog.test.tsx`). All 4 passing.
+ `yarn eslint` clean on the 3 changed/added files. Re-ran `TeamCollectionButton.test.tsx` (8
+ tests) to confirm the `CollectionTopBarControls.tsx` layout change (wrapped
+ `TeamCollectionButton` + new `ShareButton` in a flex div) didn't regress it.
+ Next action: implement step 4 (per-book panel states: signedOut/updatesAvailable/
+ offline-disabled-with-reason, Force Unlock wiring) in `TeamCollectionBookStatusPanel.tsx`.
+- 7 Jul 2026 · Per-book panel done. `TeamCollectionBookStatusPanel.tsx`: added
+ `isCloud = isCloudTeamCollection(useTeamCollectionCapabilities())` and three new
+ `StatusPanelState` values, all cloud-gated in the state-derivation effect (folder TCs never
+ produce them, since their driving fields are always undefined there, but the effect also
+ explicitly checks `isCloud` per the gating rule): `offlineDisabled` (from
+ `props.offlineDisabledReason`, checked right after the existing `invalidRepoDataErrorMsg`
+ check — a book that can't be used offline at all takes priority over lock state) renders the
+ server-supplied reason verbatim (unlocalized, same precedent as `props.error` elsewhere in
+ this file); `signedOut` (from `requiresSignIn && !signedIn`, checked where the book would
+ otherwise be "unlocked") shows `TeamCollection.SignedOut`/`SignedOutDescription` with a
+ `TeamCollection.Sharing.SignIn` button posting `sharing/showSignIn` (same action
+ `JoinCloudCollectionDialog`'s NotSignedIn case uses); `updatesAvailable` (from
+ `repoVersionSeq > localVersionSeq` on an otherwise-unlocked book) shows
+ `TeamCollection.UpdatesAvailableForBook(Description)` with the existing
+ `TeamCollection.ReceiveUpdates` button/endpoint reused from step 2. Check-in progress "modal
+ Send": for cloud TCs, the `lockedByMe` case's inline yellow progress bar is replaced by a
+ `BloomDialog` (title reuses the existing `checkingIn` string) with an MUI `LinearProgress`
+ driven by the same `checkInProgress` websocket-driven state folder TCs already use; folder
+ TCs keep the exact inline bar. Force Unlock: `ForceUnlockDialog.tsx` now posts
+ `sharing/forceUnlock` instead of `teamCollection/forceUnlock` when `isCloud` — per
+ CONTRACTS.md this maps (Wave-3) to the audited `force_unlock(book_id)` RPC ("admin; audited;
+ emits ForcedUnlock event"); no client-side audit logic needed since the RPC handles that
+ server-side. No new XLF: every string above (`SignedOut`, `SignedOutDescription`, `Sharing.SignIn`,
+ `UpdatesAvailableForBook`, `UpdatesAvailableForBookDescription`, `ReceiveUpdates`,
+ `OfflineDisabled`, and the reused `CheckingIn`) was already front-loaded or pre-existing.
+ New test: `TeamCollectionBookStatusPanel.test.tsx` (11 tests) covering the folder-TC state
+ matrix (unlocked/locked/lockedByMe/hasInvalidRepoData unchanged, plus an explicit
+ gating regression test proving cloud-shaped fields are ignored when capabilities are all
+ false) and the three new cloud states (signedOut incl. its Sign In action,
+ signedIn-so-normal-checkout, updatesAvailable incl. its Receive Updates action and the
+ no-update-when-current case, offlineDisabled's priority + verbatim reason, and the cloud
+ modal not appearing at rest). All 11 passing. `yarn eslint` clean (only the one
+ pre-existing, unrelated `checkInProgress` exhaustive-deps warning, confirmed present before
+ this change too via `git stash`). Re-ran the full `teamCollection` + `CollectionTopBarControls`
+ suites (7 files, 53 tests) — all green, no regressions.
+ Next action: implement step 5 (book thumbnails: subtle "newer version exists" marker) —
+ likely in the book-thumbnail/preview component that overlays the holder avatar (not yet
+ identified by file name; search for the existing holder-avatar-overlay code first).
+- 7 Jul 2026 · Book thumbnails done. Found the holder-avatar overlay in
+ `collectionsTab/BookButton.tsx` (the per-book thumbnail button in the collection tab's grid,
+ NOT `TeamCollectionBookStatusPanel.tsx` which is the panel for the currently-selected book):
+ it renders `` as a sibling of the thumbnail `