First slice of #17 (validate real publishing) — the code-side hardening that doesn't need live credentials.
describeBlueskyErrormaps raw failures to short, actionable messages: rate limit (429), session expired (401/403 → reconnect), image too large (413), transient 5xx, the ATProto body message for other rejects, and network failures. Previously the user sawBluesky API failed (429): …. These flow to the composer's failure toast and the queue'sfailure_reason.withFreshSessionnow handles a dead refresh token: marks the identityexpiredand surfaces "reconnect on Accounts" instead of a cryptic API error.- 7 new unit tests for the mapper (28 total);
tscexit 0;pickierclean.
Does not close #17 — the "confirm a real post lands" steps still need a real Bluesky app password. Next.