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/AGENTS.md b/AGENTS.md
index 2716eed87200..b1231876b153 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -72,6 +72,25 @@ The vscode terminal often loses the first character sent from copilot agents. So
- Do not launch Bloom with `dotnet run` or `node scripts/watchBloomExe.mjs` unless you are specifically working on the launcher scripts themselves or a better repo-supported source-aware launcher has been documented.
+- Two source-aware launchers exist; both start Vite (so the front-end hot-reloads) and pass
+ `--automation --attended`:
+ - `./go.sh` — runs Bloom under `dotnet watch`. Best for front-end work and small C# tweaks on a
+ **single** instance. The catch: the watcher holds the `output/` apphost locked, so a C# change
+ forces a full rebuild+relaunch anyway *and* nothing else can rebuild until you stop Bloom.
+ - `./run.sh` — the **build-once** launcher: it builds `BloomExe` once (Debug), then launches the
+ built `Bloom.exe` **directly**, with no C# file watcher. Prefer this for substantial C# work and
+ for running **two instances at once** (e.g. Team Collection testing). Two windows both run
+ `./run.sh`; a build lock + freshness check means only the first actually builds and the second
+ reuses that build (and its Vite). To pick up a later C# change, close Bloom (or press Enter at
+ its relaunch prompt) — `run.sh` rebuilds and relaunches while keeping Vite warm.
+ (`pnpm run run` also works; bare `pnpm run` only lists scripts.)
+ - Any `Bloom.exe` running from this repo's `output/` tree is a dev build of this worktree. Since
+ `run.sh` uses no watcher, such an instance can be stopped (and stays stopped) to free the build.
+ Identify them via the `bloom-automation` skill's status probe (HTTP `common/instanceInfo`), and
+ stop one with `killBloomProcess.mjs --http-port `. NOTE: `killBloomProcess.mjs` with no
+ args enumerates processes via `wmic`, which is absent on newer Windows 11 — prefer the
+ `--http-port`/`--pid` forms there.
+
If you create new files for temporary purposes (e.g. output or artifact or log files), be sure to clean them up when you're done and be careful not to accidentally commit them.
# Don't run pnpm build
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..48fdb395beac
--- /dev/null
+++ b/Design/CloudTeamCollections/CONTRACTS.md
@@ -0,0 +1,165 @@
+# Cloud Team Collections — frozen API contracts (v1)
+
+Changes to this file require an orchestrator commit and a version-note bump here.
+**Contract version: 1.7** (16 Jul 2026 — added the `get_collection_file_manifest` RPC,
+additive (E9): a per-file manifest for one collection-file group so the download path fetches
+only changed files pinned to their committed `s3_version_id`, mirroring `get_book_manifest`;
+the data already lived in `tc.collection_group_files`, this just exposes it for reads. No
+schema change. v1.6, 13 Jul 2026 — durable member display names, additive (John's
+13 Jul request): `tc.members` gains an editable `display_name` column; new
+`members_set_display_name(collection_id, member_id, display_name)` RPC (admin may set
+anyone's, a claimed member their own; blank clears); `members_list` rows carry
+`display_name`; `resolve_member_display` (hence `locked_by_name` everywhere it already
+appears) prefers it over the JWT-claim event capture; `get_changes` event rows gain
+`by_display_name` (the CURRENT durable name of `by_user_id`). Display rule everywhere:
+name when set, else email. v1.5, 11 Jul: 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 |
+| `get_collection_file_manifest(collection_id, group_key)` | v1.7: per-file current manifest `{groupKey, version, files:[{path, sha256, size, s3VersionId}]}` for one collection-file group, so the download path fetches only changed files pinned to their committed `s3_version_id` (E9); a never-written group returns `version 0` / empty `files`. Mirrors `get_book_manifest`. |
+| `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. v1.6: list rows carry `display_name` |
+| `members_set_display_name(collection_id, member_id bigint, display_name text)` | v1.6: sets the durable human-readable name shown in place of the email (member list, checkout status, history). Admin may set anyone's; a claimed member may set their own; blank/whitespace clears to NULL (display falls back to email); max 100 chars |
+| `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/DOGFOOD-BATCH-1.md b/Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md
new file mode 100644
index 000000000000..ff36ce093d93
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/DOGFOOD-BATCH-1.md
@@ -0,0 +1,1911 @@
+# 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: **COMPLETE — John's visual check PASSED 13 Jul 2026** ("Confirmed checking progress
+is in the right place"). Same session also hand-confirmed the batch's very first fix:
+check-in MESSAGES appear in history and are shared to teammates.
+- [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).
+- [x] [HUMAN, John] Visual check that the checkin-progress dialog appears centered over the
+ status panel during a manual checkin — PASSED 13 Jul 2026.
+
+### 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.
+ **What that question means (plain English, clarified 14 Jul 2026):** A lock is recorded on
+ the server as (user, machine, SEAT), where the seat identifies WHICH local copy of the
+ collection took the checkout (hash of that copy's folder path). The bug #0 fix makes
+ *takeover* of a lock require a matching machine+seat, and enforces it in BOTH places — the
+ client (IsEditableHere/CanTakeOverLockOnThisMachine) AND the server (checkout_book_takeover).
+ But an ordinary *check-in* (checkin_start_tx) only verifies the lock is held by the same
+ USER; it does not check that the check-in comes from the seat that holds the lock. Today the
+ only thing stopping a same-user cross-seat check-in is the client: from a second copy the
+ book shows as not-editable-here, so the UI won't let you edit/check it in. Concretely: John
+ has two copies of the Tetun collection (C:\temp\Tetun Books and the OneDrive copy) under one
+ account. If he checks a book out in copy A and then, from copy B, something got a check-in
+ through (a client bug, a bypass, or a future/alternate client), the server would accept it
+ and could clobber the version copy A is working on. The question is whether to close that gap
+ server-side — i.e. also make checkin_start_tx require the caller's machine+seat to match the
+ lock's — as defense-in-depth so the seat rule doesn't rely solely on client behavior.
+ Trade-off: it needs the client to send its seat/machine on check-in and a server migration +
+ pgTAP; risk is low and it mirrors the takeover gate we already trust. (Not urgent — no live
+ data-loss seen; the client gate holds in normal use.)
+ **DECISION (14 Jul 2026, John): WON'T DO — do not enforce seat on check-in server-side.**
+ Two reasons: (1) a client buggy or hacked enough to check in from the wrong source folder
+ could just as easily send the wrong seat checksum, so the server gate wouldn't actually
+ protect against that threat; and (2) a server-side seat requirement could get in the way of
+ recovering a collection whose local folder has legitimately been moved or renamed (which
+ changes the seat hash). The client-side editable gate remains the enforcement point. **This
+ was the last open piece of bug #0; bug #0 is now fully closed.**
+ 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. **UPGRADED from cosmetic (13 Jul human test): cloud TC identity must be the SIGNED-IN
+ account, not the registration email.** TeamCollectionManager.CurrentUser (=
+ Registration.Default.Email) drives Administrators, checkout attribution, and every
+ CurrentUserIdentity comparison — so John's Alice-signed-in instance displayed his books
+ as "checked out to john_thomson@sil.org" and every lockedBy comparison crosses
+ identities (it limps through only because the same-seat logic tolerates the mismatch).
+ For a cloud TC, CurrentUserIdentity should resolve to the signed-in cloud email.
+ Dogfood workaround (= what the e2e harness does): impersonate.txt line 1 in the
+ collection folder overrides the TC user (alice's copy got one 13 Jul).
+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).
+7. **[NEW, 13 Jul human test] Create-over-stale-cloud-link reports FAKE SUCCESS.** John
+ shared Tetun Books, whose folder still carried Thursday's cloud link (server rows wiped
+ by e2e resets): ConnectToCloudCollection correctly threw
+ TeamCollectionLinkConflictException, but HandleCreateCloudTeamCollection's catch calls
+ request.PostSucceeded() (comment: avoid a double toast) — so CreateTeamCollection.tsx
+ advanced to "Your Team Collection is ready. Invite your team from the Sharing panel" with
+ NO server row, NO link rewrite, and no Sharing panel (nothing was created). The
+ ErrorReport.NotifyUserOfProblem dialog was not seen (may not display in this context).
+ FIX NEEDED: the reply must let the dialog distinguish failure (show the conflict message
+ + guidance) — and decide the recovery UX for "cloud link points at a collection that no
+ longer exists server-side" (same family as the deferred recovery-preconditions decision;
+ the create flow could offer re-create when the dead link's id == this collection's id).
+ Workaround applied for the human test: moved TeamCollectionLink.txt +
+ .bloom-cloud-repo-cache.json + lastCollectionFileSyncData.txt out of Tetun Books (backup:
+ Documents/Bloom/Tetun-Books-stale-tc-backup-2026-07-13).
+8. **[NEW, 13 Jul — UPSTREAM, not this branch] Debug builds die silently at the collection
+ chooser**: UrlLookup.LookupFullUrl's Debug.Assert ("provide an appropriate acceptFinalUrl
+ param when looking up a url during startup") fires via CollectionChooserApi.
+ HandleGetUnpublishedCount → GetLibraryStatusForBooks → BloomLibraryDetailPageUrlFromBookId
+ (all master code; timing/network dependent) and a failed assert TERMINATES the process
+ ("Process terminated. Assertion Failed", captured stdout 13 Jul). Release unaffected.
+ Candidate for a YouTrack issue against master, not a Cloud TC fix.
+9. **[NEW, 13 Jul human test] Client cache survives server-side collection recreation and
+ poisons everything after.** C:\temp\Tetun Books kept Thursday's
+ .bloom-cloud-repo-cache.json (lastSeenEventId 13, phantom books at seq 4) across the
+ server wipe + 13 Jul re-create of the SAME collection id. Consequences observed live:
+ the initial share push uploaded NOTHING (cache said the server already had every book at
+ the current version — zero checkin transactions ever), and Alice's checkout sent a
+ phantom Thursday book id → checkout_book raised book_not_found → "Bloom was not able to
+ check out". FIX NEEDED: a FULL get_collection_state snapshot must REPLACE the cache
+ (prune cached books absent from the snapshot), and lastSeenEventId > server max_event_id
+ must be detected as server regression → drop cache, full resync. Workaround: delete the
+ stale cache files before reopening (pending — an instance is still holding them).
+10. **[NEW, 13 Jul] `pnpm go` (watchBloomExe.mjs) always passes `--automation`**, so HUMAN
+ dogfooding runs under automation semantics: progress-dialog problems auto-close
+ (BrowserProgressDialog, 11:19:07 log — this hid the failed initial push), and
+ NonFatalProblem reports go to stdout only. The dev launcher needs a way to run WITHOUT
+ automation mode (flag through go.mjs → watchBloomExe), or humans keep not-seeing errors.
+12. **[NEW, 13 Jul] Cloud identity silently leaks between instances on one Windows account.**
+ John signed in "as fred" in a second instance, yet that instance's server calls ran as
+ ALICE (proof: collection-file + book downloads succeeded — fred isn't a member and would
+ get 403; tc.members/events show zero fred activity). Cause: the DPAPI CloudTokenStore is
+ per-Windows-user (shared by every instance), and the shared MRU auto-opened Alice's
+ C:\temp copy (which also collided with her running instance — IOException on the
+ .bloomCollection). Access control HELD server-side; the failure is client identity UX.
+ Design question queued: what is a UI sign-in's scope (instance? machine?), and should
+ BLOOM_CLOUDTC_USER-style per-instance pinning be a first-class dev affordance?
+ Dogfood rule until then: one PowerShell (env-pinned user) + one collection folder per
+ identity.
+13. **[NEW, 13 Jul, UX] "Send All" is hard to find**: it lives at the bottom of the TC
+ dialog's STATUS tab, but the dialog deliberately opens on the History tab when there are
+ no important status messages — John saw only "Sync + Close". The create-success message
+ also points at the Sharing panel, not here. Consider surfacing Send All on the status
+ panel or making the Status tab default when there are uncommitted local books.
+ **DECISION (14 Jul 2026, John): leave "Send All" where it is — no location/UX change
+ needed. The Send-All-discoverability half of this item is CLOSED; the admin-role half
+ below is still open.** ALSO
+ 13 Jul: create-time `Settings.Administrators = CurrentUser` stamps the REGISTRATION
+ email (bug #4 family) — locked Alice out of her own collection's settings until the
+ .bloomCollection was hand-patched; and TeamCollectionApi.isUserAdmin
+ (OkToEditCollectionSettings) reports false for the server-side admin — the admin flag
+ for cloud should come from the server membership role.
+11. **FIXED 13 Jul — join-card dedup is now identity-aware (John's ruling).** Bob (invited,
+ claimed member) got NO join card for Tetun because the dedup suppressed by cloud id
+ against EVERY local copy in the machine-wide chooser list — including ALICE's C:\temp
+ copy. Fix: ComputeJoinCards now takes (id, lastKnownUser) pairs + the signed-in email
+ and suppresses only when the copy's TeamCollectionLastKnownUser.txt matches the
+ signed-in account (case-insensitive); unknown/missing marker or another account's copy
+ → the card SHOWS (CloudJoinFlow's scenario matching handles merge/conflict at join
+ time, unchanged). New SharingApi.SignedInEmailForJoinCards;
+ GetLocalCloudCollectionIds → GetLocalCloudCopies. 8 chooser tests incl. the exact live
+ scenario (other account's copy → card shows); filter 435/435. Also cleaned this
+ morning's stale-state workarounds: poisoned Thursday caches purged from C:\temp\Tetun
+ Books (bug #9's trigger), both Chodri copies un-teamed (dead collection); backups in
+ C:\temp\stale-cloud-tc-backup-2026-07-13.
+14. **FIXED 13 Jul — only ONE pending invitation per collection was possible.** Found by
+ the display-name test fixture, present since the original schema:
+ `members_claimed_user_uq` was `UNIQUE NULLS NOT DISTINCT (collection_id, user_id)`, so
+ two unclaimed rows (user_id NULL) collided — inviting a second person before the first
+ claimed made members_add die with 23505 (its ON CONFLICT clause targets the email
+ constraint, not this one). Contradicted the schema's own documented intent ("claimed
+ user unique"); dogfooding never tripped it because invites happened to alternate with
+ claims. Fix: 20260713000002 recreates the constraint with default NULLS DISTINCT.
+18. **FIXED 13 Jul PM — a teammate's rename arrived as a DUPLICATE book (two local folders,
+ one instance id) on the receiving side.** John's live report: after Bob's retitle
+ check-in (server row renamed to "Tetun moon and cap", seq 3, unlocked — verified),
+ Alice ended up with old-name AND new-name folders, same id, same content, phantom
+ checkout displays, "both selected at once". Root cause: an identity-first REGRESSION —
+ base `NewBookRenamedFrom`'s heuristic ("a local folder with repo status can't be the
+ rename source") assumes name-keyed status; under identity-first the old-name folder
+ resolving (by id) to the renamed row is precisely what MARKS it as the rename source,
+ so rename detection always failed, the renamed book was treated as NEW and
+ auto-downloaded beside the old folder, and each later sync happily refreshed the
+ old-name folder's content via its identity binding (explaining "same content incl. the
+ new title in both"). FIX: `NewBookRenamedFrom` is now `protected internal virtual`
+ (base body byte-identical — folder TCs keep their heuristic);
+ CloudTeamCollection overrides it with an exact instance-id comparison (repo name → row
+ instance id → local folder carrying that id under a different name). Guards added in
+ the two auto-download sweeps (`QueueMissingRepoBooksForBackgroundDownload` + a
+ re-check in `DownloadMissingBookInBackground`, both inside CanAutoApplyRemoteChanges-
+ gated paths, i.e. cloud-only at runtime): a repo book that is a rename of an existing
+ local book is NOT "missing" — the rename is applied by the next sync's
+ rename-from-remote pass, which is already identity-driven (GetRepoBooksByIdMap) and
+ works for cloud unchanged. CLEANUP: Alice's old-name duplicate backed up to
+ C:\temp\stale-cloud-tc-backup-2026-07-13\ and removed; her copy now has one folder per
+ id. 3 new CloudIdentityFirstLookupTests.
+17. **FIXED 13 Jul PM — new/local-only books in a cloud TC showed "checked out to John1"
+ (the REGISTRATION identity).** John's live report from Bob's copy; Alice's copy only
+ looked right because C:\temp\Tetun Books\impersonate.txt makes her registration EQUAL
+ the account. The base status JSON stamps `who` (plus whoFirstName/whoSurname and
+ currentUserName) from TeamCollectionManager.CurrentUser for books whose checkout is
+ purely local (new local book / not in repo). FIX in
+ TeamCollectionApi.AddCloudBookStatusFields (same method as the earlier currentUser
+ override, bug #4 family): when `who` equals the base registration currentUser, rewrite
+ it to the signed-in account email and clear the registration first/surname;
+ currentUserName now also carries the account identity (avatar dialog). A real repo
+ lock's `who` (a member email) is untouched. 2 new TeamCollectionApiCloudTests.
+ SECOND ROUND (John: "still showing John1"): the who-rewrite was aimed at the wrong
+ field — `who` was ALREADY the account (CurrentUserForStatus resolves to
+ CloudTeamCollection.CurrentUserIdentity); the REAL leak is whoFirstName/whoSurname,
+ which base WhoHasBookLockedFirstName stamps from the REGISTRATION name for
+ new/local-only books, and the UI prefers the name fields over `who`. Fix:
+ AddCloudBookStatusFields clears whoFirstName/whoSurname whenever isNewLocalBook — the
+ display falls back to `who` (account email). The earlier who-rewrite kept (covers a
+ signed-out registration leak). +1 test — NOT yet run (John's two instances hold
+ output\Debug); run the filter when they close.
+ RESIDUAL of the same family (already in bug #13's tail): create-time
+ Settings.Administrators and isUserAdmin still use registration identity.
+16. **FIXED 13 Jul PM — `pnpm go` + `--automation` silently killed Bob's startup on any
+ sync warning (bug #10's sharp edge, now with a live victim).** John's report: Bob
+ exited without any message when switching to the OneDrive Tetun copy, twice. SIL log
+ (Log-tmpcgri51/tmp2dcrx1.txt): the startup TC sync reported problems (run 1: the
+ perfectly LEGITIMATE "Renaming the local book 'The Moon and the Cap' because there is
+ a new one with the same name" — the conflict machinery working as designed on a
+ folder whose meta.json id (db07d1f3…, some earlier derivative) differed from the repo
+ book's (ca252af0…)) → BrowserProgressDialog's automation gate auto-closed the dialog
+ ("problems were reported … auto-closing because Bloom is in automation mode") →
+ Application.Run() returned → silent exit. Every relaunch re-tripped it. FIX: new
+ `--attended` flag (Program.StartupAttended / UnattendedAutomation = automation &&
+ !attended): --automation keeps the ready-handshake, port summary, and single-instance
+ bypass, while the four no-human UI policies (BrowserProgressDialog auto-close ×2,
+ HtmlErrorReporter notify suppression, NonFatalProblem stdout redirect,
+ ProblemReportApi report suppression) now gate on UnattendedAutomation.
+ watchBloomExe.mjs (pnpm go) passes --attended by default (opt out with
+ BLOOM_GO_UNATTENDED=1 for agent-driven CDP runs that must never block on a dialog);
+ the E2E harness's own launch.ts passes plain --automation and keeps full unattended
+ behavior. +4 ProgramTests; filter 454/454. RESIDUAL for next E2E pass: agent `pnpm go`
+ sessions now show real dialogs unless BLOOM_GO_UNATTENDED=1 is set — update agent
+ runbooks. CLEANUP done with it: Bob's OneDrive copy had TWO folders with instance id
+ ca252af0 ("The Moon and the Cap" re-downloaded by the sync + "Tetun moon and cap",
+ Bob's uncommitted retitle from the shared-folder chaos) — the retitle folder is backed
+ up at C:\temp\stale-cloud-tc-backup-2026-07-13\ and removed; "The Moon and the Cap1"
+ (db07d1f3, the sync's own conflict rename) left in place as a plain local book. Run
+ 2's problem message was never logged (blank) — with dialogs visible again it will
+ identify itself on screen if it recurs.
+15. **FIXED 13 Jul PM — identity-first book resolution (John's ruling: "the status of a
+ particular record by instanceID in the database is the source of truth for that book's
+ state" — ALWAYS by instance id for a local folder, not merely as a name-miss fallback,
+ so an offline-created local book X never wears the status of a teammate's checked-in
+ book that happens to share the name X).** CloudTeamCollection.ResolveBookId: a local
+ folder resolves ONLY by its meta.json bookInstanceId → _bookIdByInstanceId (unreadable
+ id ⇒ null, fail-safe "local-only", never a name guess); the name index applies only
+ when no local folder exists (repo-name queries, e.g. GetBookList names). Applied to
+ status/lock/seat/version/delete/fetch/casing/history-filter lookups; PutBookInRepo now
+ resolves by instance id ONLY (a name match could have checked in over a different
+ same-named book — the worst form of this bug); RenameBookInRepo is now a documented
+ no-op (_pendingRenameBookId deleted — identity resolution makes the bridge redundant);
+ GetRepoBookFile deliberately stays name-based (base contract: repo names, per
+ NewBookRenamedFrom/GetRepoBooksByIdMap). Placeholder merge
+ (CollectionApi.ComputeNotYetDownloadedBookEntries) also suppresses by local instance
+ id, killing the phantom "shadow" card. Per John: FolderTeamCollection untouched — all
+ changes are CloudTeamCollection overrides + the cloud-only placeholder path;
+ TeamCollection.cs/FolderTeamCollection.cs byte-identical. Tests: new
+ CloudIdentityFirstLookupTests (renamed-checked-out book keeps its row+avatar; John's
+ offline same-name conflict scenario; repo-name lookup unaffected; no-meta folder
+ fail-safe) + placeholder-suppression test; two OkToCheckIn fixtures updated to give
+ their local folder its real meta.json identity; PolledChanges fake server made
+ tolerant of the auto-apply background download (pre-existing cross-thread flake its
+ strict assert caused). Filter (Cloud|TeamCollection|SharingApi|CollectionApi) 442/442.
+ ORIGINAL DIAGNOSIS (13 Jul PM, John's live report; server verified CONSISTENT — one row,
+ "The Moon and the Cap", still locked by Bob, seq 1, no rename/check-in events).
+ Cloud renames are carried only at check-in (RenameBookInRepo + checkin-start
+ proposedName, by design), but every client-side repo lookup is by FOLDER NAME:
+ `TryGetBookStatusJsonFromRepo` → `TryGetCachedBook(bookFolderName)` → `_bookIdByName`.
+ After the local folder rename, the new name misses the index (only the in-memory
+ `_pendingRenameBookId` bridges it, and only during PutBook), so the checked-out book
+ reads as "not in repo" → treated as a NEW LOCAL BOOK: editable, no checkout avatar
+ (Bob's symptom). Meanwhile `GetBookList()` still returns the server (old) name with no
+ matching local folder → the progressive-join merge shows a phantom cloud-download
+ placeholder of the same book (the "shadow" card). FIX SHAPE (queued, not yet applied):
+ resolve by IDENTITY when the name lookup misses — the local folder's meta.json
+ bookInstanceId → `_bookIdByInstanceId` (and/or the local status file's `oldName`, which
+ HandleBookRename already records) — in TryGetBookStatusJsonFromRepo /
+ IsBookPresentInRepo / TryLockInRepo-family lookups; and suppress a placeholder when a
+ LOCAL book with the same instance id exists regardless of name. SESSION CONFOUND,
+ important: BOTH of John's instances were running against ONE local copy —
+ `C:\Users\JohnThomson\OneDrive\Documents\Bloom\Tetun Books` (Bob's pull-down; NOTE
+ Documents is OneDrive-redirected). Seat proof: both live locks carry seat
+ 6d5c0907085da326 = that folder; Alice's C:\temp\Tetun Books is bed69996f9b9d0e1 and
+ holds NO current lock. So "Alice got the rename instantly" via the SHARED FILESYSTEM,
+ not the server, and her instance likely auto-opened Bob's copy from the MRU after a
+ restart (the MRU trap again — and my C# push at ~14:5x likely recycled the dotnet
+ watch, killing both test instances). Two Blooms sharing one collection folder is
+ unsupported; her doubled cards are partly that artifact, but Bob's symptoms reproduce
+ single-instance and are the real bug.
+
+## Progress log
+(orchestrator appends: date · what was just completed · EXACT next action)
+- 16 Jul 2026 (E9 DONE — collection files download only what changed, pinned + consistent;
+ John chose Option 2) · Collection-file group downloads used to LIST the S3 prefix and re-fetch
+ every object every time (no change detection; a mid-write listing could read an uncommitted
+ mix). Discovery: the committed per-file manifest ALREADY existed durably in
+ tc.collection_group_files (path/sha256/size_bytes/s3_version_id, replaced atomically by
+ collection-files-finish) — just not exposed for reads, so NO schema migration was needed (and
+ John confirmed we're free to change the local test DB anyway). Fix (commit 66de46e910): new
+ get_collection_file_manifest(collection_id, group_key) RPC mirroring get_book_manifest
+ (CONTRACTS v1.7); client GetCollectionFileManifest; DownloadCollectionFileGroup rewritten to
+ fetch the manifest and hand the files to CloudBookTransfer.DownloadFiles — the SAME path book
+ content uses, which already skips files whose local sha256+size match and downloads the rest
+ pinned to their committed s3VersionId (a consistent snapshot). Delete-extras keys off manifest
+ paths; dead S3-listing code + its Amazon.S3.Model/WebLibraryIntegration usings removed. A
+ never-written group returns version 0 / empty files (handled gracefully). Verified live on the
+ dev stack: pgTAP 95/95 (6 new get_collection_file_manifest tests); C# required filter 452/452;
+ e2e-2 collaboration loop PASS (join downloads collection files, Send/Receive byte-identical).
+ · EXACT next action: remaining open items for John — (a) pull the 8 mid-session master commits,
+ (b) re-run PR #8052 regen vs current master + Greptile, (c) remaining refactors E7/R5/R6/R13.
+- 16 Jul 2026 (E8 DONE — incremental history fetch; John chose approach A) · The history panel
+ (SharingApi sharing/history) called get_changes(0) on every open, re-downloading the entire
+ event log each time. Now (commit 9ceeded909) it persists the cursor (max_event_id) alongside the
+ on-disk history cache and fetches only events past it (get_changes(cursor)), merging into the
+ cache. Safe: the event log is append-only and get_changes' cursor is exclusive (confirmed vs
+ pgTAP test 7c: get_changes(since=max) returns empty). Cache format gained a cursor
+ ({MaxEventId, Events}); an old bare-array cache loads with cursor 0 → one full refetch → re-saved
+ in the new format (back-compat, offline view preserved). TRADE-OFF (John's call, "go with A;
+ switch to B if users complain about stale names"): already-cached rows keep the book name /
+ author display name they were fetched with until the cache rebuilds (book renames + admin
+ display-name edits don't retroactively update old rows). Fallback if that bites: approach B
+ (refetch-only-when-changed). Refactor: extracted testable LoadHistoryCache / SaveHistoryCache /
+ MergeHistory statics + a shared FilterToCurrentBook; 6 new unit tests. Required filter 452/452.
+ · EXACT next action: remaining open items for John — (a) pull the 8 mid-session master commits,
+ (b) re-run PR #8052 regen vs current master + Greptile, (c) remaining refactors E7/E9/R5/R6/R13.
+- 16 Jul 2026 (E2 DONE — cache collection-scoped download credentials) · download-start returns
+ read-only S3 creds scoped to the WHOLE collection prefix (tc/{cid}/*, ~1h); every book +
+ collection-file-group download was fetching them separately (N+ edge-function calls + server STS
+ AssumeRole per join). Now cached (CloudTeamCollection.GetCollectionDownloadLocation, commit
+ a9e817f616): one fetch serves a whole join/Receive. Re-fetched only near the creds' stated
+ expiration (captured via new CloudS3Location.ExpiresAtUtc + ParseCredentialExpiration; MinValue
+ when absent ⇒ non-cacheable) or on account change. Risk review (John asked): creds are
+ collection-scoped + read-only so cross-book reuse is correct; no new exposure; only expiry to
+ manage, handled by honoring the server's real expiration (no TTL assumption). BUG caught by the
+ existing SyncAtStartup_TeammateRenamedBook test during verification: an absent-expiration
+ (MinValue) cache entry made the staleness check compute `MinValue - margin` → underflow throw on
+ the 2nd call; fixed by adding the margin to `now` instead. 3 new tests (reuse-within-expiry /
+ refetch-when-expired / refetch-without-throwing-when-no-expiration). Required filter 446/446.
+ · EXACT next action: remaining open items for John — (a) pull the 8 mid-session master commits,
+ (b) re-run PR #8052 regen vs current master + Greptile, (c) remaining refactors E7/E8/E9/R5/R6/R13.
+- 16 Jul 2026 (decision C RESOLVED — 'updates available' now includes books checked out elsewhere)
+ · John's ruling: GetUpdatesAvailableCount should count books checked out by ANOTHER user (a
+ reviewer can still receive the latest checked-in version; and it's wrong for the badge to switch
+ off — without the update ever being received — just because someone else took the book out).
+ Fix (commit d2bd21847d): GetUpdatesAvailableCount now excludes only books checked out HERE
+ (was: any locked book), computed via the SAME StatusFromCachedBook + IsCheckedOutHereBy path
+ ReceiveAllUpdates uses — so the badge count and what Sync receives can't drift (this closes the
+ predicate-drift item flagged during the batch-3 ReceiveAllUpdates extraction). The top-bar
+ TeamCollectionButton's "Updates Available (N)" label reads this count, so it now stays on while a
+ book is checked out elsewhere. Test updated to pin both halves; required filter 443/443. · EXACT
+ next action: remaining open items — John decides (a) pull the 8 mid-session master commits, (b)
+ re-run PR #8052 regen vs current master, (c) the remaining efficiency/reuse refactors
+ (E2/E7/E8/E9/R5/R6/R13).
+- 16 Jul 2026 (DpapiCloudTokenStore WIRED — cloud login now survives restart; John decision B
+ resolved) · The REPORT-ONLY "DpapiCloudTokenStore never wired" item is DONE (John: we want a
+ real cloud login to survive restart; make the progress we can now). Root state found: CloudAuth's
+ whole persistence mechanism (Save session/refresh-token on sign-in+refresh, Load+refresh at
+ startup, Clear on sign-out, proactive-refresh timer) was already built AND unit-tested via a fake
+ store — the ONLY gap was CloudAuth.CreateInitialized injecting the in-memory store, so nothing
+ reached disk. Fix (commit f4c5dbf2e3): CreateInitialized now injects DpapiCloudTokenStore (all
+ three call sites — CloudTeamCollection/TeamCollectionManager/SharingApi — flow through it). Dev
+ env-override path unaffected (re-signs from BLOOM_CLOUDTC_USER before consulting the store). Added
+ an end-to-end test using the REAL DpapiCloudTokenStore (session saved by one CloudAuth restored by
+ a fresh one over the same on-disk file = simulated restart); the dev provider's refresh follows
+ the identical Load-then-Refresh path Firebase will use, so the mechanism is fully verified now
+ without a live Firebase account. Required filter 443/443. WHAT STILL WAITS FOR FIREBASE: only a
+ literal real-account restart round-trip — the code path is identical and proven. · EXACT next
+ action: unchanged open items — John decides (a) pull the 8 mid-session master commits, (b) re-run
+ PR #8052 regen vs current master, (c) the remaining deferred refactors (E2/E7/E8/E9/R5/R6/R13) +
+ the ReceiveAllUpdates/GetUpdatesAvailableCount predicate drift.
+- 16 Jul 2026 (merge origin/master — John: "rebase on current master") · Merged origin/master
+ (tip 5ed9403367, #8071 go-launcher-parallel-worktrees) into cloud-collections as commit
+ 760695d960 (a merge, NOT a literal rebase — the SQUASH-PLAN forbids rebasing this 270-commit
+ branch). **Launcher reconciliation per John's guidance** (minimize changes to existing files;
+ reuse master's helpers; no duplicate methods): go.mjs, processTree.mjs, watchBloomExe.mjs,
+ build.js resolved to MASTER's versions (our branch now carries ZERO diff on them — adopts
+ #8071's parallel-worktree design + reapOrphanedBloomDevStacks, drops our behavior-neutral
+ /simplify of them). The cloud build-once launcher stays as pure additions (run.sh, run.mjs,
+ automationReady.mjs, childOutput.mjs, viteDevServer.mjs); run.mjs rewritten to use master's
+ killProcessTree (dropped our duplicate terminateChildProcess) and reapOrphanedBloomDevStacks
+ (dropped our sweepStaleWorktreeNodeProcesses). CollectionCard.tsx: union-merged master's card
+ redesign (isCurrentCollection highlight + Chip/metadata row) with our join-card support, wiring
+ the "Get" join cue into master's new inline metadata row; join-card tests updated for the new
+ title element (body1 not h5). Verified: C# required filter 442/442; auto-merged
+ CollectionChooserApi + BloomS3ClientTests 39/39; collection + teamCollection vitest 100/100
+ (run sequentially — the parallel full-suite stall is the KNOWN Windows vitest flake, hang-fix in
+ vitest.setup.ts intact, NOT a merge regression); all launchers pass node --check. · NOTE:
+ origin/master advanced 8 more commits DURING this session (fba6b758ab etc.: speedUpCSharpTests,
+ fasterIntegrationTests, a Version6.4 merge, BL-16532 toolbox fix) — the merge captured
+ master-as-of-merge-time; those 8 are not yet pulled (surfaced to John). · EXACT next action:
+ John decides whether to also pull the 8 newer master commits now; then the PR #8052 regen should
+ be re-run against CURRENT origin/master (now that cloud-collections contains it — the earlier
+ regen was against the merge-base); the launcher-reconciliation question is CLOSED.
+- 16 Jul 2026 (final — John authorized the force-push + freed memory) · Two things closed:
+ (1) **PR #8052 force-pushed** — regenerated `cloud-tc-for-review` (byte-identical to
+ cloud-collections) pushed with --force-with-lease (51da40f8f0→98bc3524ac); **Greptile
+ re-triggered** via `@greptile-apps review` comment (issuecomment-4989391847) describing the
+ /simplify pass. (2) **Live e2e now PASSES** — with memory freed (49% free vs the earlier 14%),
+ `e2e-1-create-share` + `e2e-2-collaboration-loop` both PASSED (2/2, 3.9 min) through the live
+ Supabase+MinIO stack. This confirms the earlier about:blank failures were purely the documented
+ memory-pressure flake, and LIVE-VERIFIES R2 (BuildDefaultClient S3 client, up+down), R1
+ (ListAllObjects in DownloadCollectionFileGroup, via e2e-2's join/receive), and the batch-3
+ ReceiveAllUpdates extraction end-to-end. · **Still for John (morning):** master-merge +
+ launcher reconciliation (origin/master's #8071 go-launcher change vs our 85ba119594); the
+ REPORT-ONLY decision list; an optional go.sh/run.sh launcher smoke (e2e bypasses those scripts).
+ Dev stack is still up. Working tree clean (only session-local .claude/settings.json untracked).
+- 16 Jul 2026 (later — "FINISH EVERYTHING" per John; /simplify application COMPLETE & COMMITTED)
+ · John chose "Finish everything" for the remaining optional work, and gave an uninterrupted
+ multi-hour window (machine already set never-sleep-on-AC; verified, no change needed).
+ **All /simplify batches are now done, verified, and committed on cloud-collections as 6 clean
+ logical-chunk commits** (plus the RESUME doc commit 046cd6f855):
+ 1c7b66e0fd Cloud TC C#: simplify/reuse (dead-API removal + S3/path reuse) [batch 1+2]
+ 3d928bfa07 Cloud TC UI: shared sign-in form + test-render helper
+ 88720668fd Cloud TC E2E: shared waitForSharingReady + paths header
+ 6ea968daec Cloud TC server: shared paths + S3 upload verify (deno 33/33)
+ 85ba119594 Cloud TC launchers: shared process-tree + automation-ready
+ 68b5b8c634 Cloud TC C#: file organization (partials + provider split) [batch 3]
+ · **Batch-2 finished**: R11-use (reuse hoisted AvailablePath for the cloud .bloomSource path),
+ R1 (DownloadCollectionFileGroup → S3Extensions.ListAllObjects), R2 (deleted duplicate
+ BuildS3Client, reuse CloudBookTransfer.BuildDefaultClient) + a NEW test pinning E4's
+ localManifest hash-reuse. · **Batch-3 finished** (subagent did A/B/C mechanically, I did D;
+ all independently re-verified): CloudTeamCollection.CollectionFiles.cs + TeamCollection.
+ AutoApply.cs partials, Dev/FirebaseCloudAuthProvider split out of CloudAuth.cs, and
+ HandleReceiveUpdates' loop → CloudTeamCollection.ReceiveAllUpdates. Verified as PURE
+ relocations — member-signature counts preserved exactly (CloudTeamCollection 71→71,
+ TeamCollection 130→130, CloudAuth 34→34).
+ · **Verification**: C# required filter 442/442 (0 fail); front-end vitest 593 pass (the only 5
+ failures are pre-existing/unrelated talkingBookSpec — dir byte-identical to master); eslint
+ clean on changed files; deno edge-fn tests 33/33.
+ · **Live e2e — ATTEMPTED, BLOCKED BY ENVIRONMENT (not a regression).** Brought up the full dev
+ stack (podman → supabase [was wedged; clean stop+start fixed] → MinIO on the supabase net →
+ functions serve --env-file). Ran e2e-1 + e2e-2, then e2e-1 alone: ALL failed with the
+ DOCUMENTED "WebView2 stuck at about:blank" signature at connectOverCdp (launch.ts:490) — i.e.
+ the UI never navigated, BEFORE any cloud/S3 logic runs. Root cause is the README's documented
+ memory/CPU-pressure flakiness: only 14-16% RAM free (below the ~18-20% danger line), driven by
+ the user's Chrome (51 procs, ~5 GB) + the WSL/Podman VM (~3 GB) — none of which are mine to
+ kill. NOT caused by this session's code (first failure was at the initial FOLDER-collection
+ launch; 442 tests pass; front-end is built; single-instance also fails so it isn't
+ port-collision). R1/R2 are consolidations onto ALREADY-live-proven code (BuildDefaultClient =
+ the builder all uploads already use and that passed prior e2e; ListAllObjects = the helper
+ BloomS3Client already uses in prod), so unit+integration+reuse analysis is strong. Dev stack
+ LEFT UP so John can run e2e-1/e2e-2 himself in a less-loaded environment as the final
+ confirmation. · **NOT run**: a live go.sh/run.sh launcher smoke — the e2e harness launches
+ Bloom.exe directly (not via go.sh/run.sh), so it doesn't exercise the launcher refactor;
+ validated statically instead (all shared-helper imports resolve to real exports; node --check
+ + eslint clean). Recommend John do a quick go.sh/run.sh launch (he runs these constantly).
+ · **PR #8052 regen — DONE LOCALLY & BYTE-IDENTICAL, but the force-push is CORRECTLY BLOCKED
+ pending John's explicit OK.** Regenerated cloud-tc-for-review (9 review-grained groups) against
+ the MERGE-BASE master commit e476af54d6 (NOT latest origin/master — see the master-merge flag
+ below); regen-bucket reported 0 unmatched (all new files bucket correctly), and regen-rebuild's
+ identity check confirmed "tree matches cloud-collections exactly". The branch is ready locally;
+ `git push origin cloud-tc-for-review --force-with-lease` was denied by the safety classifier
+ (rewriting/force-pushing a public PR branch's history wasn't explicitly authorized) — CORRECT;
+ it needs John's go-ahead. Greptile will need re-triggering after the push.
+ · **FLAG FOR JOHN — master-merge is a separate call (conflict risk on OUR launcher work):**
+ cloud-collections is 14 commits behind origin/master (15 Jul PM); origin/master HEAD is
+ "go-launcher-parallel-worktrees" (#8071), an UPSTREAM launcher change that almost certainly
+ collides with commit 85ba119594's launcher-simplify. Merging latest master + reconciling the
+ two launcher refactors is a judgment call left to John. (The PR regen above sidesteps this by
+ basing on the merge-base; a "proper" regen against current master should follow the
+ master-merge.)
+ · **REPORT-ONLY decisions still pending for John** (from the PAUSE-#2 list, none actioned this
+ session): DpapiCloudTokenStore is NEVER WIRED (real sessions won't survive restart); E2 cache
+ DownloadStart creds (~1h TTL join speedup); E7 rename-scan cost/poll; E8 history refetches whole
+ log; E9 collection-file group re-downloads unchanged files; A3 AddCloudBookStatusFields JSON
+ surgery behind virtual seams (skipped deliberately); R6 remaining hooks → useWatchApiData; R13
+ getApiDataOnce dedup; R5 shared C# cloud test-fixture builder. ALSO: the ReceiveAllUpdates /
+ GetUpdatesAvailableCount predicate drift (Sync receives books locked by another user; the badge
+ excludes them) — documented inline at ReceiveAllUpdates, decide whether to unify.
+ · **EXACT next action on resume:** (1) John: decide + authorize the `git push origin
+ cloud-tc-for-review --force-with-lease`, then re-trigger Greptile on #8052. (2) John: run live
+ e2e-1/e2e-2 in a less memory-loaded environment (stack is up) as the R1/R2 confirmation, plus a
+ quick go.sh/run.sh launcher smoke. (3) John: decide the master-merge + launcher-reconciliation.
+ (4) Work through the REPORT-ONLY decisions. Nothing else is in-flight; working tree is clean
+ (only untracked LIVE-STATUS.md scratch + session-local .claude/settings.json).
+- 16 Jul 2026 (RESUME from PAUSE #2 — verification pass; WIP is HEALTHIER than the pause note
+ feared) · Resumed the /simplify application. Working tree matched the PAUSE #2 doc exactly
+ (67 modified + 5 new source files + untracked .claude/settings.json — nothing lost; top
+ stash is unrelated Version6.4). **Findings, all now resolved or characterized:**
+ - **C# COMPILES** (the pause note's "tree may not compile" worry was unfounded — R1/R2/
+ R11-use are unstarted *additional* cleanups, not half-applied breakage). BloomExe builds
+ clean. The ONLY real breakage: 4 call sites in CloudBookTransferTests.cs still passed the
+ pre-E4 arg list to `UploadChangedFiles` (missing the new `localManifest` param) →
+ CS7036. FIXED: inserted `null` for `localManifest` at all 4 sites (behavior-preserving —
+ doc says null ⇒ hash as before). BloomTests now compiles.
+ - **C# required filter GREEN: 441/441** (non-live: `(~Cloud|~TeamCollection|~SharingApi)
+ &!~LiveTests`), 0 failures. (Doc's 460 pre-batch-2 count included ~19 LiveTests, excluded
+ here since the stack was not confirmed up; zero failures either way.) CloudBookTransfer
+ fixture 10/10.
+ - **vitest full suite TRIAGED: 593 passed, 5 failed, 5 skipped.** All 5 failures are in
+ `bookEdit/toolbox/talkingBook/talkingBookSpec.ts` ("sentence splitting" audio-checksum
+ tests) — a directory this branch NEVER touched (git diff vs origin/master is empty; last
+ commit Mar 2026; clean in working tree; none of its deps are in the changed set; global
+ test infra untouched). **Conclusion: pre-existing / environmental, NOT caused by the
+ refactor.** Every refactored teamCollection/collection test passes.
+ - **eslint (changed front-end files): 0 errors.** Fixed 3 warnings in CreateTeamCollection.tsx
+ (dropped unused `showDialog`/`closeDialog` from the useSetupBloomDialog destructure — a
+ leftover of the CreateCloudTeamCollection extraction; `==`→`===` on the boxesChecked
+ gate). CreateTeamCollection + CreateCloudTeamCollection vitest 14/14.
+ - e2e: prior agent reported tsc/eslint-level clean; live run still queued (desktop/stack gated).
+ **State of REMAINING documented work (unchanged, still open):** batch-2 R1 (DownloadCollection
+ FileGroup→S3Extensions.ListAllObjects), R2 (delete CloudTeamCollection.BuildS3Client, reuse
+ CloudBookTransfer.BuildDefaultClient), R11-use (reuse hoisted AvailablePath) — all UNSTARTED,
+ and R1/R2 touch AWSSDK-v4 S3 client construction (want live MinIO verify); batch-3 file org
+ (partials + provider split, large mechanical reorg); full pass + live run.sh/go.sh smoke;
+ commit in logical chunks; regen #8052 + re-trigger Greptile (needs `gh auth`, was broken at
+ pause). **My 7 line-fixes above are LEFT UNCOMMITTED with the rest of the WIP** (matching this
+ batch's established pattern — code WIP uncommitted, state in this doc), fully reproducible from
+ this entry. **EXACT next action:** paused to confirm scope with John — how far to push the
+ remaining OPTIONAL/quality work (R1/R2/R11-use + batch-3 file org) in this session given the
+ token concern that triggered the pause, and whether `gh` is now authenticated for the PR regen.
+- 15 Jul 2026 (PAUSED #2 — John: "put this task on hold until the next session; too many
+ tokens"). Continuation of the entry below; tree has UNCOMMITTED WIP (~67 modified + 5 new
+ files). **What changed since the previous PAUSE entry:**
+ - C# batch-1 is COMPLETE and GREEN: fixed the CloudEnvironmentTests compile break, finished
+ the whole batch-1 tail (CloudBookTransfer dead `alreadyUploadedThisTransaction` param
+ dropped from UploadChangedFiles + both CloudTeamCollection call sites + tests;
+ CloudCollectionMonitor dead Stop() deleted + Dispose doc'd terminal; SharingApi
+ serializes `emailVerified`, SignedInEmailForJoinCards→CurrentAuth().CurrentEmail,
+ RegisterWithApiHandler doc'd; CloudTeamCollection CollectionIdForCloud→CloudCollectionId
+ and TryGetBookIdForTests→GetBookIdByNameIndexForTests with callers; TryTakeOverLock doc
+ reworded). Required filter ran **460/460 PASSED** (down from 465 because dead-API tests
+ were deleted with their APIs).
+ - MID-PAUSE INCIDENT (resolved): an old stash mis-applied during the previous pause left
+ conflict markers in 9 files foreign to this branch's work + a spurious untracked
+ CanvasElementManager.ts; with John's approval all 9 were restored from HEAD and the stray
+ file deleted. No stash was dropped; nothing of ours was lost.
+ - Three fix agents were then relaunched and STOPPED for THIS pause, each mid-flight:
+ (a) React/TS agent — finished auditing/completing the test-render-helper refactor and was
+ running the FULL vitest suite, which **exited code 1**; it was stopped while reading the
+ failure summary. UNKNOWN whether the failure is refactor-caused or pre-existing — first
+ resume job: run `pnpm exec vitest run` in src/BloomBrowserUI and triage.
+ (b) e2e agent — verification came back CLEAN (stragglers converted, tsc/eslint-level check
+ passed); it was stopped during scratchpad cleanup (it made a HEAD-baseline checkout in the
+ session scratchpad with a node_modules junction — harmless, dies with the session).
+ Treat e2e work as DONE pending one final look.
+ (c) C# batch-2 agent — had applied E1 (GetRepoBooksByIdMap protected virtual + cloud
+ override from _cache.GetAllBooks()), E3 (poll early-return), E4 (manifest hashes into
+ UploadChangedFiles), E6 (display-name failure TTL), R12 (CloudAuth.CreateInitialized),
+ and the R11 hoist (AvailablePath→base); its last words were "Now R11 (use hoisted
+ AvailablePath), R1 (ListAllObjects), R2 (delete BuildS3Client)" — so R11's USE side, R1,
+ R2 are NOT done, and NOTHING of batch 2 has been compiled or tested. The C# tree may not
+ compile.
+ **EXACT next action on resume:** (1) `git diff` the C# files to see batch-2 agent's actual
+ edits; finish R11-use/R1/R2 (specs in the entry below); run the required filter (baseline
+ was 460/460 pre-batch-2). (2) Triage the vitest full-suite failure; finish React/TS
+ verification (vitest/eslint/typecheck). (3) C# batch 3 (file org — specs below). (4) Full
+ test pass + live run.sh smoke (launchers still unverified live). (5) Commit in logical
+ chunks (NOTE: `.claude/settings.json` is untracked session-local config — do NOT commit it;
+ add files explicitly, no `git add -A`). (6) Regenerate #8052 via orchestration/regen-*.sh,
+ re-trigger Greptile. (7) Final report to John: fixed/skipped summary + REPORT-ONLY decision
+ list (below, incl. DpapiCloudTokenStore never wired).
+- 15 Jul 2026 (PAUSED MID-REFACTOR — tokens ran out; tree has UNCOMMITTED WIP that DOES NOT
+ COMPILE yet) · John asked for a /simplify-style quality review of the whole branch; 5 review
+ agents produced ~49 findings; application was in progress when paused. **STATE:**
+ - DONE + verified, uncommitted: (a) supabase edge functions — shared
+ `_shared/paths.ts` (key layout) + `captureVerifiedUploads` in `_shared/s3.ts` (Map + 8-way
+ bounded parallel verify) used by both finish fns; `stubAssumeRole`→test_support;
+ `S3_WRITE_ACTIONS` shared — deno 33/33 BEFORE AND AFTER + deno check clean. (b) launcher
+ scripts — `terminateChildProcess` (with signalFirst option) in processTree.mjs used by
+ go/run/watchBloomExe; new `automationReady.mjs` (prefix + scanner) used by run+watchBloomExe;
+ watchBloomExe now uses pipeChildOutput — node --check + eslint clean (needs a live
+ go.sh/run.sh smoke later).
+ - DONE, uncommitted, MY C# batch-1 so far: BookVersionManifest Diff API deleted (+tests);
+ CloudJoinFlow.ListMyCollections+CreateAndJoinCollection deleted; CloudCollectionClient
+ UndeleteBookRpc/RenameCheck deleted + UnlockBookRpc/ForceUnlockRpc/DeleteBookRpc renamed
+ without Rpc suffix (callers in CloudTeamCollection.cs updated; LockTests test method
+ renamed); CloudAuth AccountSwitched/SignedOut events + EventArgs + raiseEvent param deleted
+ (+tests rewritten as SignIn_WithDifferentAccount_ReplacesIdentity; stale comments fixed in
+ CloudTeamCollection.cs + MemberTests); CloudEnvironment S3Bucket + FirebaseProjectId props +
+ env reads deleted. **BROKEN RIGHT NOW: CloudEnvironmentTests still asserts env.S3Bucket
+ (~line 18, ~71) and env.FirebaseProjectId (~76, ~88) — delete those assertion lines (and the
+ two env entries "BLOOM_CLOUDTC_FIREBASE_PROJECT_ID"/S3_BUCKET in the sandbox test dict) to
+ compile.**
+ - Two fix agents were STOPPED MID-WORK (partial uncommitted edits, unverified): React/TS agent
+ (was doing: extract CreateCloudTeamCollection.tsx from CreateTeamCollection.tsx — likely
+ done; shared DevSignInForm.tsx — likely done; useSharingLoginState→useWatchApiData; shared
+ test-render helper — was mid-way through the 13 test files, stopped at SharingPanel.test.tsx/
+ NewerVersionAvailableMarker.test.tsx) and e2e agent (edits done — waitForSharingReady in
+ harness/bloomApi.ts replacing ~14 poll copies + paths.ts header — but verification NOT run).
+ On resume: run vitest/eslint/typecheck for BloomBrowserUI and finish/verify both.
+ - REMAINING C# work I had queued (batch 1 tail): CloudBookTransfer drop dead
+ `alreadyUploadedThisTransaction` param (both CloudTeamCollection call sites pass throwaway
+ sets — removes the lock complexity too); CloudCollectionMonitor delete dead Stop() + doc
+ Dispose; SharingApi: serialize `emailVerified` in HandleLoginState reply (1 line, makes the
+ declared TS field truthful), simplify SignedInEmailForJoinCards→CurrentAuth().CurrentEmail
+ (+line ~560 IsSignedIn), doc RegisterWithApiHandler; CloudTeamCollection rename
+ CollectionIdForCloud→CloudCollectionId + doc (caller TeamCollectionApi.cs:293), rename
+ TryGetBookIdForTests→GetBookIdByNameIndexForTests (caller LiveTests:224); TeamCollection.cs
+ reword TryTakeOverLock doc ("true = may proceed as lock holder incl. nothing-to-take-over").
+ - BATCH 2 (efficiency/reuse, NOT started): E1 GetRepoBooksByIdMap→protected virtual + cloud
+ override from _cache.GetAllBooks() (kills 3 network calls/book at startup); E3
+ OnPolledChanges early-return when get_changes empty + cursor unchanged (kills full-cache
+ Save every 60s); E4 pass local manifest hashes into UploadChangedFiles (kills double SHA256);
+ E6 CurrentUserDisplayName failure retry TTL ~30s; R1 DownloadCollectionFileGroup→
+ S3Extensions.ListAllObjects; R2 delete CloudTeamCollection.BuildS3Client, reuse
+ CloudBookTransfer.BuildDefaultClient (make internal static); R11 hoist
+ FolderTeamCollection.AvailablePath→base TeamCollection, reuse in GetAvailableBloomSourcePath;
+ R12 CloudAuth.CreateInitialized factory (3 dup sites: TeamCollectionManager:707,
+ CloudTeamCollection:130, SharingApi:144).
+ - BATCH 3 (file org, NOT started; John asked explicitly): extract partial
+ CloudTeamCollection.CollectionFiles.cs (~310-line banner-delimited cluster ~1462-1770);
+ partial TeamCollection.AutoApply.cs (queue machinery ~104-244 + ProcessAutoApplyRemoteChange
+ + DownloadMissingBookInBackground + QueueMissingRepoBooks...); split DevCloudAuthProvider +
+ FirebaseCloudAuthProvider out of CloudAuth.cs; move HandleReceiveUpdates' loop into
+ CloudTeamCollection.ReceiveAllUpdates (NOTE: its predicate skips checked-out-HERE while
+ GetUpdatesAvailableCount excludes locked-by-ANYONE — drift to surface to John, don't silently
+ unify).
+ - REPORT-ONLY for John (decisions): DpapiCloudTokenStore is NEVER WIRED (real sessions won't
+ survive restart — wire at the 2 CloudAuth construction sites when AuthMode==Cloud, or delete
+ until Firebase ships); E2 cache DownloadStart credentials (~1h TTL, big join speedup); E7
+ rename-scan cost per poll; E8 history refetches whole log each open (use since-cursor); E9
+ collection-file group re-downloads unchanged files (compare size/ETag from listing); A3
+ AddCloudBookStatusFields JSON-surgery could move behind virtual WhoHasBookLocked* seams
+ (skipped deliberately: identity logic just live-verified, not worth re-destabilizing); R6
+ remaining hooks (useSharingMembers etc.) could share a reload-capable useWatchApiData; R13
+ getApiDataOnce dedup (sharingApi/teamCollectionApi cached-promise idiom); R5 shared C# cloud
+ test-fixture builder (9 copy-pasted harness blocks). Full agent reports also survive in the
+ session tasks dir (a6925…=reuse, a095b…=simplify, af6da…=efficiency, ad850…=altitude,
+ a48a9…=naming .output files).
+ **EXACT next action on resume:** (1) fix CloudEnvironmentTests compile break; (2) finish C#
+ batch-1 tail; (3) run required filter (expect ~465 minus deleted diff/event tests, plus fix
+ fallout); (4) verify/finish the React+e2e agents' partial work (vitest/eslint/typecheck);
+ (5) batches 2-3; (6) full test pass, live-smoke run.sh, commit in logical chunks, regenerate
+ #8052 via regen scripts, re-trigger Greptile; (7) give John the fixed/skipped summary + the
+ REPORT-ONLY decision list.
+- 15 Jul 2026 (doc cleanup, per John) · Removed the spent orchestration scratch documents from
+ the branch: all 11 agent launch prompts (`orchestration/*.prompt.md` — one-time kickoff
+ prompts; the durable per-task specs stay in `tasks/*.md`, which code comments reference),
+ `BUG0-OPTION-A-SKETCH.md` (bug #0 implemented as option (a) and fully closed — the outcome is
+ recorded in this file's OUTSTANDING BUGS #0), and `notes-item7-progressive-join.md` (scouting
+ notes with stale line numbers; item 7 shipped). RESUME.md was TRIMMED, not removed: its
+ still-operative rules (mandatory C# test filter, review-before-merge, dev-stack bring-up,
+ environment quirks) are what this file and IMPLEMENTATION.md point at; its stale Wave-4
+ status and prompt-launching machinery are gone. Three e2e harness comments that cited
+ 09-e2e.prompt.md now cite src/BloomTests/e2e/README.md (same rules, kept doc). KEPT (the
+ durable set): CloudTeamCollections.md (design), CONTRACTS.md, GOING-LIVE.md (real
+ buckets/DB/deploy), IMPLEMENTATION.md (wave/merge history), tasks/*.md (specs+findings,
+ code-referenced), docs/ (walkthrough, unit-test setup), notes/write-book-status-audit.md
+ (code-referenced), this file (state), SQUASH-PLAN.md + regen-*.sh (needed until merge),
+ server/dev + firebase + e2e READMEs. Mentions of the deleted files in OLD progress-log
+ entries here and in tasks/IMPLEMENTATION logs are historical records and were left alone.
+ **EXACT next action:** regenerate cloud-tc-for-review + force-push (diff shrinks by the
+ deleted docs), re-trigger Greptile; then back to the standing next steps (item 10 [HUMAN],
+ promote #8052 when ready).
+- 15 Jul 2026 (AM — fresh-eyes review of the 14 Jul work; three real fixes) · John asked for a
+ review of yesterday's session. Findings, all fixed + regression-tested (465/465 on the
+ required filter):
+ 1. **Rename-apply guard was unsound (data-loss risk).** The bug B fix guarded the
+ SyncAtStartup rename-apply block with LOCAL status (`!GetLocalStatus(..).IsCheckedOut()`),
+ but cloud checkouts never stamp local status (TryLockInRepo is RPC+cache only) — so
+ "check out → retitle → RESTART before check-in" would rename the folder back to the repo
+ name and overwrite the checked-out edits from the repo. Local status can also carry a
+ STALE teammate lock (accept-remote-lock sync writes repo status locally), wrongly
+ SKIPPING a legitimate rename. Fix: guard on the REPO lock (`IsCheckedOutHereBy(repo
+ status)` — same machine-local rule as QueueMissingRepoBooksForBackgroundDownload) plus a
+ `NewBookRenamedFrom(newName)==thisFolder` confirmation, which doubles as the
+ folder-TC gate (base implementation always answers null when the folder has repo status,
+ so folder TCs provably never enter the block). New tests:
+ CloudSyncAtStartupTests.SyncAtStartup_TeammateRenamedBook_RenamesLocalFolderInPlace and
+ SyncAtStartup_OwnRenameMidCheckin_DoesNotRevertRenameOrClobber (+ per-key S3 test harness).
+ 2. **Display-name cache ignored account switch.** CurrentUserDisplayName cached on a
+ session-wide boolean, so after Bob signs out and Alice signs in (batch item 9), Alice's
+ new local books showed BOB's display name. Fix: cache keyed by the signed-in email. New
+ test: AddCloudBookStatusFields_AccountSwitch_RefreshesDisplayName.
+ 3. **run.sh freshness check was effectively dead after any edit.** It compared source mtimes
+ against the apphost Bloom.exe, which incremental builds usually don't touch (only
+ Bloom.dll changes) — so the skip-build fast path never fired again after the first C#
+ edit. Fix: compare against the newest of Bloom.exe/Bloom.dll; also include the repo-root
+ Directory.Build.* files in the source scan (master's new Directory.Build.props affects
+ builds).
+ Also: the review-branch regeneration scripts now live IN THE REPO
+ (orchestration/regen-bucket.sh + regen-rebuild.sh, referenced from SQUASH-PLAN.md) instead
+ of a session-scratchpad that dies with the session. Reviewed-and-fine: incremental-download
+ seeding, upload-race lock, LockedSeat/LocalVersionSeq cache fixes, ExperimentalFeatures
+ exact-token match, XLF fix, launcher architecture. **EXACT next action:** regenerate
+ cloud-tc-for-review with the new scripts, force-push #8052, re-trigger Greptile.
+- 14 Jul 2026 (PM #6 — CLEAN REVIEW CHECKPOINT: Devin + Greptile both satisfied) · Greptile
+ re-review of #8052 head 8fbe66e4ea PASSED (9m27s) with NO new findings; the prior XLF P1 is
+ fixed in the new head and its thread is resolved/outdated; all checks green (Greptile,
+ pr-automation, track); PR MERGEABLE, still draft. **Review status: both bots clear.** Devin
+ (via the fork slice-reviews) → 7 actionable, 5 fixed + 1 dismissed + 1 deferred. Greptile
+ (full diff) → clean after the XLF P1 fix. **Remaining before human review/merge are the known
+ [HUMAN] items only:** item 10 real Bloom Library web upload/download + the deferred
+ S3ForcePathStyle production-AWS check (GOING-LIVE.md 4.3); and John's live spot-checks. No
+ agent action pending on the bot gauntlet. EXACT next action (future): item-10 production
+ validation; when ready, promote #8052 from draft to ready-for-human.
+- 14 Jul 2026 (PM #5 — review branch refreshed with all fixes; Greptile re-running) · Re-merged
+ origin/master (now 7209ba3bc1 → cloud-collections df580ff6bf); required filter 462/462; pushed.
+ cloud-tc-for-review REGENERATED (bucket now also assigns ExperimentalFeaturesTests.cs → g6;
+ --no-verify; byte-identical to cloud-collections — empty diff) and force-pushed → **PR #8052
+ head 8fbe66e4ea, MERGEABLE**, now carrying all Devin-finding fixes. Greptile re-review triggered
+ (@greptile-apps review) and PENDING on the new head. **Fork Devin-probe cleanup DONE:** PRs
+ JohnThomson #3/#4/#5 closed, branches devin-probe-{base,client-core,server,backend-api} deleted
+ (remote + local). Devin slice-review harvest fully complete (7 actionable → 5 fixed, 1 dismissed,
+ 1 deferred to item 10). **EXACT next action:** when Greptile finishes on 8fbe66e4ea, gather +
+ triage its findings; fix any real ones on cloud-collections and re-regenerate/force-push.
+- 14 Jul 2026 (PM #4 — Devin findings triaged + FIXED) · Commit `99bfb1e102` on cloud-collections
+ (pushed) fixes 5 of the 7 actionable Devin findings; 462/462 required-filter + new regression
+ test; eslint clean. **Fixed:** (1) CloudBookTransfer upload race — alreadyUploadedThisTransaction
+ now accessed only under resultGate; (2) CloudRepoCache.RecordCheckinFinish now clears LockedSeat
+ on release; (3) ApplyFullSnapshot now carries LocalVersionSeq across the swap (was wiping it →
+ needless re-downloads); (6) ExperimentalFeatures now exact-token match (was substring — cloud/
+ folder TC flags collided) + regression test CloudAndFolderTeamCollectionTokensDoNotCollide;
+ (7) CollectionHistoryTable.tsx now labels numeric-type ≥100 cloud incident events (were blank).
+ **DISMISSED (5) BloomS3Client.cs:130** — false positive: the flagged `internal static
+ CreateAmazonS3Client(config,credentials)` is a NEW helper, not a visibility downgrade; the methods
+ bloom-harvester actually overrides (GetAccessKeyCredentials, the bucketName CreateAmazonS3Client
+ overload) are unchanged `protected virtual` — no cross-repo break. **DEFERRED (4)
+ CloudEnvironment.cs:144 S3ForcePathStyle** to item 10 (GOING-LIVE real-AWS validation): the logic
+ `= !IsNullOrEmpty(S3Endpoint)` is correct for dev/MinIO (DefaultS3Endpoint is the MinIO URL) but
+ would force path-style on real AWS; the right production config (leave S3Endpoint empty, or an
+ explicit force-path-style override) is exactly what item 10's production validation must settle.
+ **EXACT next action:** re-regenerate cloud-tc-for-review from cloud-collections (bucket.sh/
+ rebuild.sh, --no-verify), force-push #8052; check/handle Greptile's re-review of the new head;
+ delete the throwaway fork probe PRs/branches (JohnThomson #3/#4/#5, branches devin-probe-*).
+- 14 Jul 2026 (PM #3 — ALL Devin slice findings gathered; triage/fix next) · Heads unchanged:
+ cloud-collections `5c8275bc9a`, cloud-tc-for-review `b45e5e21be` = #8052 (MERGEABLE). Devin
+ reviews of all three fork slices are COMPLETE. **Full actionable inventory to triage/fix on
+ cloud-collections:**
+ - #3 client-core (g5): BUG CloudBookTransfer.cs:241 `alreadyUploadedThisTransaction` ISet.Add
+ inside Parallel.ForEach = data race (CONFIRMED real); BUG CloudRepoCache.cs:404 lock seat not
+ cleared on check-in release (stale cache); BUG CloudRepoCache.cs:270 full snapshot wipes local
+ version tracking → needless re-downloads; INVESTIGATE CloudEnvironment.cs:144 S3ForcePathStyle
+ always true (non-empty default endpoint) → breaks real AWS (ties to item 10); INVESTIGATE
+ BloomS3Client.cs:130 protected→internal-static breaks bloom-harvester subclass.
+ - #4 server (g2+g3): 0 Bugs, 0 Investigate. 6 Informational (3 are "already fixed by a later
+ migration"; 1 worth a glance: rpc.ts:65 user values interpolated into PostgREST query strings
+ w/o URL-encoding — Devin rated Informational).
+ - #5 backend+API (g6+g7): BUG ExperimentalFeatures.cs:20 enabling cloud TC feature silently
+ enables the folder one too + disabling folder corrupts the cloud flag; BUG HistoryEvent.cs:36
+ a new (cloud) history event type renders BLANK in the history table. Informational (skip):
+ FolderTeamCollection PutBook checkinComment not passed; RemoteBookAutoApplyQueue.cs:83-91
+ case-sensitivity mismatch between dedup set & priority-move (glance-worthy, rename-adjacent);
+ SharingApi static auth/client never disposed; a positive null-ref-fix note; a test-only virtual.
+ Net: **7 actionable** (5 from g5, 2 from #5), plus 2 glance-worthy Informationals (rpc.ts URL-encode,
+ auto-apply-queue case-sensitivity). Greptile re-review of #8052 new head still not posted — check.
+ **EXACT next action:** triage each of the 7 against the code; fix the real ones on cloud-collections
+ (begin with the CONFIRMED CloudBookTransfer race). Then Greptile #8052, re-regenerate + force-push,
+ delete fork probe branches/PRs (#3/#4/#5, branches devin-probe-*).
+- 14 Jul 2026 (PM #2 — SESSION-SAVE for machine sleep; fixes + Devin-slicing review in flight) ·
+ **Branch heads:** cloud-collections = `812703d56a` (origin in sync); cloud-tc-for-review =
+ `b45e5e21be` (origin in sync) = PR #8052 head, MERGEABLE. Working tree clean.
+ **Code shipped this session (all committed + pushed on cloud-collections, ride to #8052 via the
+ regen):**
+ - Build-once dev launcher `run.sh` (+ `run.mjs`, shared `viteDevServer.mjs`/`childOutput.mjs`;
+ `go.mjs` refactored onto them; Program.cs suppresses the DEBUG "Attach debugger" modal under
+ --automation; AGENTS.md documents both launchers). Commit c1665fecdc.
+ - Bug A (new-local-book avatar shows account display name, not email): CloudTeamCollection
+ `CurrentUserDisplayName` cached from MembersList; TeamCollectionApi stamps it. Commit 3d240de7a7.
+ - Bug B (teammate-rename round-trip): SyncAtStartup now detects the remote rename by instance id
+ even when statusJson is non-null (cloud identity-first) and renames the local folder IN PLACE
+ (no duplicate, no stale name). Commit 0b27f6a6f6. LIVE-VERIFIED by John.
+ - Incremental Receive: FetchBookFromRepo seeds staging from the local folder so DownloadFiles'
+ hash-skip re-downloads only changed files (rename refresh + all Receives). Commit 696958aff8.
+ - Greptile P1: launch-crashing `--` in an XLF (CollectionTab.BookNotYetDownloaded) →
+ replaced with ';'. Commit acf3ba9272.
+ - Decisions recorded (see items #0, #13 above): Send All stays put; server-side cross-seat
+ check-in enforcement = WON'T DO (bug #0 fully closed).
+ **Preflight / review state:**
+ - origin/master (51e467d5de) merged into cloud-collections (clean, 812703d56a); required-filter
+ 460/460. cloud-tc-for-review regenerated per SQUASH-PLAN (byte-identical, `--no-verify` on the
+ 9 regen commits — John-approved, since the content already passed hooks on cloud-collections;
+ this made the rebuild seconds instead of timing out on the per-commit hook). Force-pushed → #8052.
+ - **Devin can't review #8052 (246 files "too large").** So we review the feature in SLICES via
+ throwaway PRs ON JOHN'S FORK (JohnThomson/BloomDesktop) to keep the main repo clean. Mechanism:
+ base branch = exact master commit `devin-probe-base`; each slice branch checks out that group's
+ files from cloud-collections; stacked where dependent. Slices → fork PRs:
+ - #3 client-core (g5, 19 files, base devin-probe-base) — **Devin DONE, 3 Bugs + 2 Investigate
+ + 3 Info** (below).
+ - #4 server (g2+g3, 39 files, base devin-probe-base) — Devin ANALYZING (triggered).
+ - #5 backend+API (g6+g7, 54 files, base devin-probe-client-core so g5 types are context) —
+ Devin ANALYZING (triggered).
+ - **g5 (#3) Devin findings — NOT yet triaged/fixed:** Bugs: (1) CloudRepoCache.cs:404 lock SEAT
+ not cleared when a check-in releases the lock (stale cache metadata); (2) CloudRepoCache.cs:270
+ full server snapshot wipes local version tracking → needless re-downloads (matches a queued
+ follow-up); (3) CloudBookTransfer.cs:241 `alreadyUploadedThisTransaction` (plain ISet) `.Add()`
+ inside a Parallel.ForEach = data race — **CONFIRMED real by code read.** Investigate: (a)
+ BloomS3Client.cs:130 protected→internal-static on a method bloom-harvester (separate repo)
+ extends; (b) CloudEnvironment.cs:144 S3ForcePathStyle always true (non-empty default endpoint)
+ → breaks real AWS in production (ties to item 10 GOING-LIVE). Info (skip): CloudSession setters,
+ CloudAuth timer race (benign), S3Extensions null-guards OK.
+ - Greptile on #8052 NEW head (b45e5e21be): only pr-automation shows so far; Greptile's re-review
+ not yet posted — CHECK next session.
+ **EXACT next actions (resume here):**
+ 1. Gather Devin findings from fork PRs #4 (server) and #5 (backend+API) — devin-review skill,
+ chrome-devtools isolated context, URLs app.devin.ai/review/JohnThomson/BloomDesktop/pull/{4,5}.
+ 2. Triage ALL Devin findings (g5 #3 + #4 + #5) against the code; fix the real ones on
+ cloud-collections (start with the confirmed CloudBookTransfer.cs:241 race). Also handle
+ Greptile's fresh #8052 review.
+ 3. After fixes: re-run required filter, re-regenerate cloud-tc-for-review (bucket.sh/rebuild.sh
+ in scratchpad — rebuild.sh now uses --no-verify), force-push #8052.
+ 4. Clean up the throwaway fork probe branches + PRs (JohnThomson #3/#4/#5, branches
+ devin-probe-base / -client-core / -server / -backend-api) when the review harvest is done.
+ **Tooling notes:** regen scripts live in this session's scratchpad (bucket.sh updated so all 253
+ diff files bucket into the 9 groups, 0 unmatched; rebuild.sh has --no-verify). Master added
+ `build/agent-dotnet.sh|ps1` + Directory.Build.props: builds/tests into a private per-terminal tree
+ so a running Bloom no longer blocks builds (`build/agent-dotnet.sh test --filter ...`).
+ Guard: plain `dotnet test` under the new Directory.Build.props left spurious `Bloom.sln` /
+ `BloomTests.csproj` edits (dropped BloomExe project+ref) — discard them (`git checkout --`) if they
+ reappear; the committed versions are correct.
+- 14 Jul 2026 (John pausing — low Fable credits; preflight kicked off) · All instances
+ closed; full required filter 459/459 (includes bug #17-round-2's new test, previously
+ unrun). origin/master merged into cloud-collections (clean, 17 files, f5a00c1cae) and
+ cloud-tc-for-review REGENERATED per SQUASH-PLAN (same 9 review-grained commits, head
+ 8ac48df0db, byte-identity verified — empty diff vs cloud-collections), force-pushed;
+ PR #8052 now MERGEABLE at the new head with all dogfood fixes through bug #18.
+ Bot gauntlet triggered: pr-automation (Devin) run 29341744792 in progress;
+ @greptile-apps review comment posted (246 files > auto limit). Regeneration gotcha
+ recorded: the identity check REQUIRES cloud-collections to be up to date with
+ origin/master first (stale master files otherwise show as diff; one BloomExe.csproj
+ merge artifact forced a second rebuild — cheap). Also: bash scripting note — $GROUPS is
+ a readonly bash builtin; don't use it as a variable name (cost one puzzled retry).
+ NEXT (when agent returns): gather Devin/Greptile findings from PR #8052 and
+ triage/fix; then remaining [HUMAN] items. FOR JOHN meanwhile (no agent needed):
+ (1) restart Bob → verify new/local-only books now say bob@dev.local (bug #17 fix);
+ (2) rename round-trip retest: Bob checkout → retitle → check in → Alice should get ONE
+ renamed folder (bug #18 fix), watch for the "renamed by a teammate" message;
+ (3) display names: set names via the Sharing-panel pencil and verify "checked out to
+ " + history show them; (4) item 10 [HUMAN]: real Bloom Library web
+ upload/download (AWSSDK v4 validation, GOING-LIVE.md 4.3); (5) decide bug #0 follow-up
+ (server-side refusal of same-user cross-seat check-in?) and bug #13 UX (Send All
+ discoverability); (6) if Alice's "My first test" blocks editing (lock carries the
+ OneDrive seat), admin Force Unlock from the Status panel; (7) housekeeping when done:
+ restore sleep timeouts (powercfg /change standby-timeout-ac 120; standby-timeout-dc 3).
+- 13 Jul 2026 (Sunday PM #2 — **member display names**, John's request, CODE + SQL TESTS
+ DONE; bug #14 found+fixed) · "Show who has a book checked out (and similar) by a
+ human-readable name, email as fallback; admins edit the name in the Sharing panel."
+ Server (CONTRACTS.md bumped to v1.6, additive): 20260713000001 adds
+ tc.members.display_name; members_set_display_name RPC (admin sets anyone's, a claimed
+ member their own, blank clears, ≤100 chars); members_list rows carry display_name
+ (DROP+CREATE, re-granted); resolve_member_display prefers the durable column over the
+ JWT-claim event capture — so locked_by_name in get_collection_state/get_changes/
+ get_book_manifest picks it up with no signature changes; get_changes event rows gain
+ by_display_name (the CURRENT durable name). 20260713000002 fixes bug #14 (see
+ OUTSTANDING BUGS). Both migrations applied to the live local stack via
+ `supabase migration up` (NO db reset — John's Tetun server data untouched);
+ `supabase test db` 89/89 (24 new in 03_tc_member_display_name_test.sql: auth matrix
+ admin/self/other/non-member, trim/clear/too-long, precedence, full pipeline via
+ get_collection_state + get_changes). C#: SharingApi.ToApprovedMember maps display_name
+ → name; new sharing/setDisplayName endpoint; CloudCollectionClient.MembersSetDisplayName;
+ ToBookHistoryEvent UserName preference by_display_name → by_user_name → by_email;
+ CloudTeamCollection.StatusFromCachedBook now puts the whole display name in
+ lockedByFirstName (surname null — both TS consumers render that cleanly; lockedBy STAYS
+ the email because the panel compares it with currentUser for lockedByMe). UI:
+ SharingPanel member rows get an admin-only pencil → inline input (Enter/blur commits
+ trimmed, Escape cancels, empty clears; all spans/inputs, no new divs, per the CSS
+ hazard above); sharingApi.setDisplayName. Vitest 12/12 (5 new); eslint/tsc clean.
+ SharingApiTests updated (+2 new C# tests) but **the C# suite has NOT been run**: John's
+ Alice instance (pnpm go, dotnet watch PID 67472) is live from THIS worktree, so
+ `dotnet test` would fight the locked output\Debug — ALSO NOTE the watcher has likely
+ already picked up this session's C# edits (may have rebuilt/restarted Alice) · Next:
+ run the C# required filter once Alice closes; John verifies the pencil in the Sharing
+ panel + a "checked out to " status; follow-ups queued: member self-service name
+ UI, folder-TC parity n/a (folder TCs already use registration first/surname).
+- 13 Jul 2026 (Sunday PM — John's UI niggles fixed; **CSS HAZARD recorded**) · Three niggles
+ from live testing, all pushed: (1) create-success message now says "Team Collection panel"
+ instead of "Sharing panel" (CreateTeamCollection.tsx + XLF, John's call: fix the message,
+ don't rename the panel); (2) "(experimental)" dropped from both the Cloud Team Collections
+ checkbox label (AdvancedSettingsPanel.tsx — it already sits in an "Experimental Features"
+ list) and the "Share this collection on the Bloom sharing server" button
+ (TeamCollectionSettingsPanel.tsx); (3) Sharing-panel row alignment fixed (23b636f966).
+ **HAZARD for anyone adding UI to the Team Collection settings panel:** the legacy rule
+ `#teamCollection-settings div:not(.no-space-below) { margin-bottom: 10px; }`
+ (TeamCollectionSettingsPanel.less) uses an ID selector, so it outranks every
+ emotion-class rule and silently adds 10px below EVERY div descendant — inside a
+ `align-items: center` flex row this lifts div children (avatar, text column, MUI Chip)
+ 10px above honestly-centered non-div siblings (select, button). Neutralize with
+ `> div { margin-bottom: 0 !important; }` on the flex row (see SharingPanel.tsx's
+ MemberRow/AddMemberRow comments); nothing weaker wins. Found by John with the inspector
+ after two rounds of margin/height guesses failed · Next: member display names (John's
+ 13 Jul request): display_name column on tc.members + admin editing in the Sharing panel
+ + "checked out to " with email fallback.
+- 13 Jul 2026 (Sunday AM — weekend recovery + second launch failure diagnosed) · The Podman
+ WSL machine stopped over the weekend: whole local stack was down (supabase functions serve
+ "failed to run docker"; podman ps connection refused). RECOVERED: podman machine start →
+ supabase stop/start → db reset (migrations + seed users) → MinIO compose up → functions
+ serve fresh → smoke 3/3 PASS. John's second `pnpm go` failure ("Bloom was started on port
+ 51040, but no vite server was available") did NOT reproduce on the next run (Bloom came up
+ fine, ready line + Vite connected): cold-start race — first post-weekend launch runs
+ Vite's dependency optimizer concurrently with a full dotnet-watch rebuild, and Bloom's
+ ReactControl.IsViteDevServerRunning allows only 400ms per origin (~1.2s total) at exactly
+ that moment. **QUEUED DX FOLLOW-UP: make the startup --vite-port validation patient**
+ (retry/longer window, mirroring go.mjs's own two-consecutive-successes poll) — NOT done
+ yet because a live dotnet watch was attached to John's running instance (never edit C#
+ under a live watcher) · Next: John's human tests in the running instance
+ (alice@dev.local / BloomDev123!, fresh collection).
+- 11 Jul 2026 (afternoon — human-test launch failure diagnosed + fixed) · John's `pnpm go`
+ timed out ("Bloom did not emit BLOOM_AUTOMATION_READY within 120000 ms"). Cause: the E2E
+ harness launches write to the SHARED per-machine user.config, so the last matrix left
+ `BloomE2E-join-auto-open` (a cloud TC whose server rows the per-scenario DB resets wiped)
+ at the top of MruProjects; pnpm go auto-opened it and the connection-refusal MessageBox
+ (native, blocking, invisible to the launcher) hung startup. FIX: removed the BloomE2E/
+ BobPlaceholder MRU entries (backup: user.config.bak-pre-mru-clean). **HARNESS FOLLOW-UP
+ (queued):** the E2E harness must stop polluting the human's MRU — snapshot+restore
+ MruProjects around a run (globalSetup/globalTeardown), or launch instances with an
+ isolated profile. Same class of hazard as the experimental-flag manipulation, but this
+ one actively breaks the next manual launch · Next: John's human tests proceed
+ (alice@dev.local / BloomDev123!; fresh collection required — old server rows are gone).
+- 11 Jul 2026 (morning — **GOLD STAMP: FULL MATRIX 14/14**, 34.4 min, one run, zero
+ failures) · The desktop-unlock watcher fired, environment verified clean, and the
+ matrix ran green end to end on the final tree (af8a92a516 = PR #8052's content +
+ batch-log commits): e2e-1 through e2e-10 + join-auto-open, including e2e-4's
+ seat-gated takeover refusal and e2e-10's legitimate same-seat takeover. This is the
+ first fully-green FULL matrix since the post-batch defect hunt began — the batch's
+ test pipeline is CLOSED. Everything that remains is John's: [HUMAN] item 3 (centered
+ check-in dialog) + item 10 (web up/download, GOING-LIVE.md 4.3), and the OUTSTANDING
+ BUGS #0 follow-up decision (server-side same-user cross-seat check-in refusal).
+ PR #8052 is ready for review whenever the human checks are done · Next: John.
+- 11 Jul 2026 (~04:30 — optional gold-stamp matrix INVALID: desktop locked again mid-run,
+ 14/14 CDP-connect failures = the locked-session signature; sleep timeouts were already
+ disabled, so this was a manual lock or an unseen policy). No leaked processes. NOT a
+ loss: the gold run was optional — every scenario has already passed on this exact tree
+ (13/14 matrix + standalone exonerations). Unlock watcher re-armed in the orchestrator
+ session; if it fires while the session lives, the matrix reruns · Next: John's items
+ (see the SQUASH-PLAN-EXECUTED entry above); rerun `yarn test` with the desktop
+ UNLOCKED whenever a gold stamp is wanted.
+- 11 Jul 2026 (early AM — SQUASH PLAN EXECUTED; PR #8052 open, #8048 closed) · Merged
+ origin/master into cloud-collections first (13 commits, RAB/spreadsheet only, zero
+ conflicts, filter 428/428). Built `cloud-tc-for-review` per SQUASH-PLAN.md: 9
+ review-grained commits, **byte-identical to cloud-collections (empty diff)** after
+ converging the prettier drift the packaging build exposed in 14 edge-function files
+ (formatting-only; deno 33/33 after; details in SQUASH-PLAN.md's executed-note). Draft
+ **PR #8052** opened with review order + caveat + bot history; **#8048 closed** with
+ pointer. REMAINING for the batch: (1) John's [HUMAN] tests — item 3 centered check-in
+ dialog, item 10 web up/download (GOING-LIVE.md 4.3); (2) John's OUTSTANDING BUGS #0
+ follow-up decision (server-side same-user cross-seat check-in refusal); (3) optionally
+ one more full matrix as the gold stamp (every scenario HAS passed on this tree; the
+ last run was 13/14 with the 14th standalone-exonerated + spec-hardened); (4) regenerate
+ cloud-tc-for-review whenever cloud-collections advances.
+- 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.
+
+---
+
+**2026-07-16 — E7 done (per-poll rename-scan is now O(missing + local), not O(missing × local)).**
+`QueueMissingRepoBooksForBackgroundDownload` runs unconditionally at the end of every poll's
+`OnPolledChanges` (60s + on-activation/reconnect) and calls `NewBookRenamedFrom(bookName)` for
+each repo book that has no local folder. The cloud override enumerated *every* local book folder
+(FileInfo-stat per meta.json, cached) to find the one sharing the repo book's instance id — so
+during a progressive join of a large collection it re-enumerated the whole local folder set once
+per still-missing book, every 60s. Network cost was already gone (E1's instance-id cache); this
+was the remaining per-poll disk cost. Fix per John's direction ("first time a poll needs the map
+from instanceID→localFolder, build it, then use it"): added a bulk-scan overload
+`NewBookRenamedFrom(string, ref object scanState)` (base delegates to the per-book method;
+[TeamCollection.cs](../../../src/BloomExe/TeamCollection/TeamCollection.cs)). The pass owns
+`scanState` as a **local**, so the index is built lazily on the first missing book, reused for the
+rest of that poll, rebuilt fresh next poll (never stale across polls), and confined to the polling
+thread (no lock, no race with the worker thread's one-off `NewBookRenamedFrom` calls). Cloud
+override builds `instanceId→folderName` once via `BuildLocalInstanceIdToFolder()` (per-folder id
+reads hit the timestamp/size-validated cache; first-enumerated wins on the pathological dup-id
+case, matching the old first-match loop). Steady state (no missing books) does zero enumeration —
+the overload is never called. One-off callers (`DownloadMissingBookInBackground`, sync's
+rename pass) keep the simple per-book path. Verified: clean build, TeamCollection filter 436/436
+(includes the teammate-rename regression tests that exercise this exact path). Note: while tracing
+this I did **not** find an empty-poll early-return I'd associated with a batch-2 item ("E3") in
+`OnPolledChanges` — it still does `ApplyDelta`/`Save`/self-healing unconditionally each poll.
+Flagged for a separate look; independent of E7.
+
+---
+
+**2026-07-16 — E3 done (skip the per-poll cache write on idle polls).** Resolves the flag in the
+E7 entry above: the batch-2 "OnPolledChanges early-return" item had never actually landed — no
+commit, and `OnPolledChanges` still did `ApplyDelta` + full `_cache.Save()` (whole repo-cache JSON
+to disk) + `RefreshIndexFromCache()` unconditionally on every 60s poll, even when the poll carried
+no changes. Done the safe way per John: `CloudRepoCache.ApplyDelta` now returns whether it actually
+mutated state (a book row upserted OR the cursor advanced); `OnPolledChanges` skips Save + index
+rebuild + the book-event pass when it returns false. Crucially the two things that legitimately
+must run on a no-change poll still run unconditionally: the **group-file** check
+(`RaiseRepoCollectionFilesChanged`, since `ApplyDelta` only inspects books+cursor, never groups)
+and the **self-healing** `QueueMissingRepoBooksForBackgroundDownload` pass (retries still-missing
+downloads during a join, when polls are legitimately empty — the exact scenario E7 concerns). The
+`previousBooksById` diff snapshot is now taken only when the poll carried book rows. Extracted the
+book-event loop into `RaiseBookEventsForPolledChanges` to keep the fast path readable. New unit
+test `ApplyDelta_ReportsWhetherAnythingChanged` pins the true/false contract E3 rides on. Verified:
+clean build, TeamCollection 436/436, CloudRepoCache 17/17 (16 + 1 new).
+
+---
+
+**2026-07-16 — R5 done (shared C# cloud test-fixture builder).** Every CloudTeamCollection unit
+fixture hand-rolled the same ~15-line setup block: temp folder + mock ITeamCollectionManager +
+CloudEnvironment(test anon key) + CloudAuth(stub provider, in-memory token store) + signed-in +
+CloudCollectionClient wired to a FakeRestExecutor + CloudTeamCollection with a mock-S3
+CloudBookTransfer, and an identical TearDown (dispose folder + reset ForceCurrentUserForTests).
+New `CloudTestHarness.Create(folderName, collectionId, currentUser=, signIn=, s3Factory=)` returns
+the pieces individually (Collection/Executor/Auth/MockTcManager/CollectionFolder) so each fixture
+stays explicit about what it drives and can still script the executor / inject a custom S3 mock;
+`Dispose()` is IDisposable and undoes the two process-global seams. Converted 5 fixtures
+(Lock, Member [signIn:false — it tests the signed-out path], IdentityFirst, AccountSwitch
+[currentUser:bob@dev.local], SyncAtStartup [s3Factory: scripted S3]); each keeps its own fields as
+aliases so no test body changed. Deliberately NOT applied to CloudCollectionClientTests /
+CloudCollectionMonitorTests (they test client/monitor directly, the former with a custom auth
+provider, and never build a whole collection) nor to SyncAtStartup's MakeCollectionWithPerKeyS3
+(builds a *second* collection reusing the fixture's folder — folding it in would bloat the helper
+or change behavior; exactly the over-consolidation to avoid). Net -123 dup lines across fixtures
+for +91 shared. Verified: TeamCollection|Cloud filter 438/438, no new warnings.
+
+---
+
+**2026-07-16 — R6 + R13 done (front-end hook reuse).** Two duplicated front-end idioms in the
+cloud-TC TS, both extracted into utils/bloomApi.ts:
+- **R13 (cached-promise dedup):** sharingApi.ts (`enabledExperimentalFeaturesPromise` /
+ getEnabledExperimentalFeaturesOnce) and teamCollectionApi.tsx (`capabilitiesPromise` /
+ getTeamCollectionCapabilitiesOnce) hand-rolled the identical "fetch at most once per page load,
+ cache the promise, expose a test-only reset" pattern (needed because per-book components would
+ otherwise fire hundreds of identical requests). New `getApiDataOnce(urlSuffix, mapResult)` keyed
+ by a Map, plus `resetApiDataOnceCacheForTests()` (clears the whole map). The two module-specific
+ reset exports remain (vitest.setup.ts calls them by name) but now delegate to the shared clear.
+- **R6 (reload-capable watch hook):** useSharingMembers hand-rolled the generation-counter +
+ websocket-refetch + manual-reload + skip-until-param-known idiom that useWatchApiData almost
+ covers. New additive `useWatchApiDataWithReload(urlSuffix?, default, ctx, event)` returns
+ `{data, reload}` and skips fetching while urlSuffix is undefined; useSharingMembers is now a
+ 4-line wrapper over it. Left useWatchApiData (used by ~12 files) untouched, and left
+ useMyCloudCollections alone (its `loading` flag doesn't fit the watch/reload shape -- forcing it
+ would be over-consolidation).
+Verified: eslint 0 errors / 0 new warnings on the 3 files; typecheck clean for them; 7 vitest
+consumer files (SettingsPanel, Dialog, BookStatusPanel, ShareButton, CollectionHistoryTable,
+BookButton, CollectionChooser) 38/38 green. No `pnpm build` run (the CodeReview worktree's Vite on
+:5173 is a different tree and was left alone).
diff --git a/Design/CloudTeamCollections/orchestration/RESUME.md b/Design/CloudTeamCollections/orchestration/RESUME.md
new file mode 100644
index 000000000000..e5fd8abd2079
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/RESUME.md
@@ -0,0 +1,48 @@
+# Cloud TC — orchestration rules & resume protocol
+
+> **Current state lives in [DOGFOOD-BATCH-1.md](DOGFOOD-BATCH-1.md)** (its progress log's
+> newest entry is the resume point). This file keeps the general working rules that batch
+> doc and IMPLEMENTATION.md refer to.
+>
+> History note (15 Jul 2026): this folder originally also held the per-task agent launch
+> prompts (`-.prompt.md`) used to build the feature in waves. All tasks are long
+> merged, so those scratch prompts were removed; the durable per-task specs and findings
+> remain in `../tasks/*.md`, and the wave-by-wave merge log is in `../IMPLEMENTATION.md`.
+
+## The durable-state rule
+
+All work state lives in **git**, never only in a conversation:
+
+- Commit after EVERY completed step — small, coherent commits; tick the step's checkbox /
+ update the item's `Status:` line in the same commit.
+- The state doc (currently DOGFOOD-BATCH-1.md) ends with a `## Progress log`; every work
+ session appends: `date · what was just completed · EXACT next action`. A resumer starts
+ by reading the newest entry.
+- A step half-done at interruption is redone from its last commit; uncommitted work found
+ in a leftover worktree is secured as a WIP commit first.
+
+## Working rules (still operative)
+
+- 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 (or use `build/agent-dotnet.sh`, which builds
+ into a private tree and avoids the lock entirely); edge-runtime containers must reach
+ MinIO as `bloom-minio:9000`, never `host.containers.internal` (hangs under Podman).
+
+## How to resume (human instructions)
+
+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 that
+file's newest progress-log entry and item Status lines, secures any uncommitted work, and
+continues with the stated next action.
diff --git a/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md b/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md
new file mode 100644
index 000000000000..a154080177b9
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md
@@ -0,0 +1,120 @@
+# Squash plan: review-grained history for the Cloud TC feature
+
+> **EXECUTED 11 Jul 2026** — `cloud-tc-for-review` built (9 commits on origin/master),
+> byte-identity verified (empty diff vs cloud-collections), pushed; draft **PR #8052**
+> opened; #8048 closed with a pointer. One wrinkle worth knowing for regeneration: the
+> pre-commit formatter diffs against origin/master on the packaging branch, so it exposed
+> (and fixed) prettier drift in 14 edge-function files that had bypassed it on
+> cloud-collections — converged by applying the formatted versions back to
+> cloud-collections (e782978f9d) BEFORE the final identity check. Regenerating after
+> cloud-collections advances: delete the branch, rerun the groups below, force-push.
+
+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**.
+
+> **Scripts (15 Jul 2026):** the regeneration is now fully scripted, right next to this file —
+> `regen-bucket.sh` (assigns the `origin/master..cloud-collections` diff to the 9 groups by
+> path pattern; **must report 0 unmatched** — extend its patterns when new files join the
+> branch) and `regen-rebuild.sh` (stages + commits each group with `--no-verify`, safe because
+> the resulting tree is byte-identical to `cloud-collections`, which already passed every hook,
+> then verifies that identity). Usage:
+> ```bash
+> git checkout -B cloud-tc-for-review origin/master
+> git diff --name-only origin/master cloud-collections > /tmp/allfiles.txt
+> Design/CloudTeamCollections/orchestration/regen-bucket.sh /tmp/allfiles.txt /tmp/groups
+> Design/CloudTeamCollections/orchestration/regen-rebuild.sh /tmp/groups
+> git push origin cloud-tc-for-review --force-with-lease
+> ```
+> Precondition unchanged: `cloud-collections` must already contain `origin/master` (merge it
+> first), or the byte-identity check fails.
+
+## 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/regen-bucket.sh b/Design/CloudTeamCollections/orchestration/regen-bucket.sh
new file mode 100644
index 000000000000..88c6f4686354
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/regen-bucket.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# Bucket the origin/master...cloud-collections diff into the SQUASH-PLAN groups.
+# Usage: bucket.sh -> writes g1.txt..g9.txt + unmatched.txt
+ALL="$1"; OUT="$2"
+mkdir -p "$OUT"
+for i in 1 2 3 4 5 6 7 8 9; do : > "$OUT/g$i.txt"; done
+: > "$OUT/unmatched.txt"
+while IFS= read -r f; do
+ case "$f" in
+ Design/CloudTeamCollections/*|Design/CloudTeamCollections.md|.github/skills/xlf-strings/*|AGENTS.md) echo "$f" >> "$OUT/g1.txt" ;;
+ supabase/migrations/*|supabase/tests/*|supabase/config.toml|supabase/snippets/*|supabase/.gitignore) echo "$f" >> "$OUT/g2.txt" ;;
+ supabase/functions/*) echo "$f" >> "$OUT/g3.txt" ;;
+ server/*) echo "$f" >> "$OUT/g4.txt" ;;
+ # Group 5: client core classes + their dedicated unit tests + csproj (AWSSDK bump)
+ src/BloomExe/TeamCollection/Cloud/CloudEnvironment.cs|\
+ src/BloomExe/TeamCollection/Cloud/CloudAuth*.cs|\
+ src/BloomExe/TeamCollection/Cloud/*AuthProvider*.cs|\
+ src/BloomExe/TeamCollection/Cloud/CloudTokenStore*.cs|\
+ src/BloomExe/TeamCollection/Cloud/CloudCollectionClient.cs|\
+ src/BloomExe/TeamCollection/Cloud/CloudRepoCache.cs|\
+ src/BloomExe/TeamCollection/Cloud/CloudBookTransfer.cs|\
+ src/BloomExe/TeamCollection/Cloud/BookVersionManifest.cs|\
+ src/BloomExe/TeamCollection/Cloud/S3*.cs|\
+ src/BloomExe/WebLibraryIntegration/BloomS3Client.cs|\
+ src/BloomExe/WebLibraryIntegration/S3Extensions.cs|\
+ src/BloomTests/WebLibraryIntegration/BloomS3ClientTests.cs|\
+ src/BloomExe/BloomExe.csproj|\
+ src/BloomTests/TeamCollection/Cloud/CloudEnvironmentTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/CloudAuthTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/FirebaseCloudAuthProviderTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/CloudTokenStoreTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/CloudCollectionClientTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/CloudRepoCacheTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/CloudBookTransferTests.cs|\
+ src/BloomTests/TeamCollection/Cloud/BookVersionManifestTests.cs) echo "$f" >> "$OUT/g5.txt" ;;
+ # Group 7: HTTP API layer + their tests (before group 6's broader TeamCollection match)
+ src/BloomExe/web/controllers/SharingApi.cs|\
+ src/BloomExe/web/controllers/CollectionChooserApi.cs|\
+ src/BloomExe/web/controllers/CollectionApi.cs|\
+ src/BloomExe/web/controllers/ExternalApi.cs|\
+ src/BloomExe/web/controllers/ProblemReportApi.cs|\
+ src/BloomExe/web/controllers/CollectionSettingsApi.cs|\
+ src/BloomExe/web/controllers/FileIOApi.cs|\
+ src/BloomExe/web/ReadersApi.cs|\
+ src/BloomExe/TeamCollection/TeamCollectionApi.cs|\
+ src/BloomExe/Workspace/*|\
+ src/BloomTests/web/controllers/*|\
+ src/BloomTests/TeamCollection/TeamCollectionApiCloudTests.cs|\
+ src/BloomTests/TeamCollection/WorkspaceModelTierTimingOrderingTests.cs) echo "$f" >> "$OUT/g7.txt" ;;
+ # Group 9: E2E harness
+ src/BloomTests/e2e/*) echo "$f" >> "$OUT/g9.txt" ;;
+ # Group 6: everything else in the TeamCollection backend + app-level automation guards
+ src/BloomExe/TeamCollection/*|\
+ src/BloomExe/Program.cs|\
+ src/BloomExe/NonFatalProblem.cs|\
+ src/BloomExe/MiscUI/BrowserProgressDialog.cs|\
+ src/BloomExe/ErrorReporter/HtmlErrorReporter.cs|\
+ src/BloomExe/Shell.cs|\
+ src/BloomExe/web/BloomServer.cs|\
+ src/BloomExe/ApplicationContainer.cs|\
+ src/BloomExe/Collection/CollectionSettingsDialog.cs|\
+ src/BloomExe/ExperimentalFeatures.cs|\
+ src/BloomExe/History/HistoryEvent.cs|\
+ src/BloomExe/SubscriptionAndFeatures/FeatureRegistry.cs|\
+ run.sh|\
+ scripts/*|\
+ src/BloomTests/TeamCollection/*|\
+ src/BloomTests/ProgramTests.cs|\
+ src/BloomTests/ExperimentalFeaturesTests.cs|\
+ src/BloomTests/ShellTests.cs) echo "$f" >> "$OUT/g6.txt" ;;
+ # Group 8: front-end + strings
+ src/BloomBrowserUI/*|DistFiles/localization/en/*) echo "$f" >> "$OUT/g8.txt" ;;
+ *) echo "$f" >> "$OUT/unmatched.txt" ;;
+ esac
+done < "$ALL"
+for i in 1 2 3 4 5 6 7 8 9; do printf "g%s: %s\n" "$i" "$(wc -l < "$OUT/g$i.txt")"; done
+printf "unmatched: %s\n" "$(wc -l < "$OUT/unmatched.txt")"
+cat "$OUT/unmatched.txt"
diff --git a/Design/CloudTeamCollections/orchestration/regen-rebuild.sh b/Design/CloudTeamCollections/orchestration/regen-rebuild.sh
new file mode 100644
index 000000000000..1bff1c9f2b67
--- /dev/null
+++ b/Design/CloudTeamCollections/orchestration/regen-rebuild.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# Rebuild cloud-tc-for-review per SQUASH-PLAN.md: 9 path-staged commits from cloud-collections.
+set -e
+GRPDIR="$1"
+cd /c/github/BloomDesktop
+
+declare -a MSGS
+MSGS[1]="Cloud Team Collections: design docs, wire contracts, and project records"
+MSGS[2]="Cloud TC server: tc schema, RLS policies, RPCs, and pgTAP tests"
+MSGS[3]="Cloud TC server: edge functions for checkin/download/collection-file transactions"
+MSGS[4]="Cloud TC dev stack: local Supabase + MinIO, seed users, S3 parity checks"
+MSGS[5]="Cloud TC client core: auth, API client, repo cache, S3 transfer (AWSSDK v4)"
+MSGS[6]="Cloud TC backend: cache-backed TeamCollection, polling monitor, join flow, background downloads, account-switch handling"
+MSGS[7]="Cloud TC API: sharing/membership endpoints, capabilities, join cards, book-list merge"
+MSGS[8]="Cloud TC UI: sign-in, sharing dialog, status panel, join cards, download placeholders"
+MSGS[9]="Cloud TC E2E: Playwright-over-CDP harness and 10 two-instance scenarios"
+
+for i in 1 2 3 4 5 6 7 8 9; do
+ echo "=== Group $i: $(wc -l < "$GRPDIR/g$i.txt") files ==="
+ # xargs handles the long path list; -d '\n' preserves paths with spaces
+ xargs -d '\n' -a "$GRPDIR/g$i.txt" git checkout cloud-collections --
+ xargs -d '\n' -a "$GRPDIR/g$i.txt" git add --
+ git commit -q --no-verify -m "${MSGS[$i]}
+
+Part of the Cloud Team Collections feature (S3 + Supabase backed Team
+Collections). This branch is a regenerable review-grained packaging of the
+cloud-collections working branch; only the final tree is test-verified (see
+Design/CloudTeamCollections/orchestration/SQUASH-PLAN.md).
+
+Co-Authored-By: Claude Fable 5 "
+ echo "=== Group $i committed ==="
+done
+
+echo "=== Verifying byte identity ==="
+if [ -n "$(git diff cloud-tc-for-review cloud-collections --stat)" ]; then
+ echo "IDENTITY CHECK FAILED:"
+ git diff cloud-tc-for-review cloud-collections --stat
+ exit 1
+fi
+echo "IDENTITY OK: tree matches cloud-collections exactly"
+git log --oneline origin/master..HEAD
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..a5ef6fb585c5
--- /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)
+
+- 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
+ the ui-wiring launch prompt (a scratch file, since removed) 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 `