PR state was written by two flows and the second one could not persist: the feedback fetch returned live PR data without saving it, and the browser patched its own store copy instead. Opening a PR's detail panel turned that tab purple while the kanban card and boot payload still read state=open, and a reload snapped it back.
The backend is now the only writer — the feedback read reconciles through SyncTaskPR and publishes github.task_pr.updated, so every surface converges and the state survives a reload. SyncTaskPR also refuses to move a row out of merged, since GitHub cannot un-merge a PR.
Splits the 2,842-line apps/backend/internal/github/service.go monolith into nine focused files, all under the 800-line revive threshold. No public API changes, no behavior changes — Service struct, constructor, and every method signature preserved verbatim.
Files: errors.go (sentinels), service.go (struct + constructor + getters), service_tokens.go, service_pr.go, service_pr_watch.go, service_reviews.go, service_issues.go, service_cleanup.go, service_pr_status.go.
Only semantic-equivalent code rewrites: CheckPRWatch hasNew collapsed to a single || expression; three narrow //nolint directives for pre-existing complexity reflagged by golangci's new: true mode after the move.
Rebased onto current main after PR #1057 (sentinel extraction) landed — took its errors.go (now 4 sentinels incl. ErrInvalidToken) and ported the %w wraps for ConfigureToken and AssociateExistingPRByURL into the split files.