A device whose journal push the hub 403s reported healthy sync between every pair of remote passes, and never showed the hub's reason for the refusal — so a user whose device was not registered to their account re-ran `bdrive login` (which the message told them to), re-checked their project permissions (write), and had nowhere left to look. Two causes, both local to the client: - Cycle recomputed st.Access from scratch at the end of every pass, including the daemon's cheap local-only ticks that never reach the hub. Three of those run between remote passes, so the daemon alternated "read-only on this project" / "access restored; syncing normally" every few seconds and `bdrive status` reported OK moments after a refused push. Now each leg records its own verdict — pull clears no-access, push records read-only or clears it — and a cycle that asked nothing leaves the last answer standing. - The hub's own sentence was summarized into "read-only (pull only)", which describes the STATUS CODE. It is the only thing that tells a device-registration refusal from a project the user really is a reader on. It now rides in SyncState.AccessReason and Result.Reason(), printed by `bdrive sync`, `bdrive status` and the daemon log, and dropped unless it passes journal.SafeText — hub text reaching a terminal. The hub's refusal also now names the upgrade: the binding is made by the login request naming its device, which a CLI older than the gate does not do, so "run `bdrive login`" alone sent that user in a circle. Hub and CLI deploy separately, so the skew is the expected state right after the gate ships. Claude-Session: https://claude.ai/code/session_01GSHsQU4pBCzKkPyPeXSwTm Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architecture diagrams
Mermaid diagrams of the current implementation, kept next to the code so PRs can update them alongside the change.
Convention: when a PR changes the structure drawn here (new/removed types, new seams, changed relationships), update the affected diagram in the same PR and add an "Architecture changes" section to the PR description that, per changed diagram:
- names exactly which types/relationships changed and how (one sentence);
- shows a Before and an After mermaid block — each an excerpt of only the affected classes and their immediate relationships, never the full diagram (Before comes from the diagram at the merge base).
The committed diagram file stays the full current state; the before/after
excerpts exist only in the PR description so reviewers see the structural
delta at a glance. A pre-PR hook (.claude/hooks/check-arch-diagrams.sh)
reminds Claude Code sessions when server code changed but no diagram did.
Together these cover every application package in the repo — every code change lands inside exactly one detail diagram's scope (plus the overview when the package map or cross-piece wiring changes):
- overview.md — system diagram: every package and surface on one page, and how they connect
- cli-sync.md — class diagram of the CLI and sync engine (
cmd/bdrive+internal/{syncer,store,journal,config,daemon,agenthooks,autostart}) - webapp-server.md — class diagram of the
bdrive serveserver (internal/webapp+ itsinternal/remoteseam) - webapp-frontend.md — module diagram of the hub's React SPA (
internal/webapp/frontend/src)
Not covered on purpose: web/docs (content site, no application code) and
cloud/ (private nested repo — its architecture lives there).