v5.0.0 — Flare

[Flare](/docs/glossary#flare) closes the inbound loop [Beacon](/docs/glossary#beacon) left half-open and hardens the run-all path. Both "Known issues" filed against Beacon — the missing GitHub PR-merge handler and the missing Slack HTTP transport — ship; the inbound surface gains OAuth, multi-host parity (GitHub Enterprise / GitLab / Bitbucket), per-IP rate limiting, Slack interactive components, and Discord guild auto-registration; the run-all silent-halt bug, the chat-tab repaint loop, and the buried `failure_reason` are all fixed; and the global settings UI is reorganised into searchable category sub-pages.

### Added

- **GitHub PR-merge handler — closes the v4.0 Beacon auto-PR loop.** New `internal/daemon/echo/handler_github.go` registered at `POST /echo/github?project=` parses `X-GitHub-Event` / `X-Hub-Signature-256` / `X-GitHub-Delivery`, resolves the per-project HMAC secret from the keyring, runs `verify.VerifyGitHub`, deduplicates against the LRU+TTL idempotency cache, narrows on `event == "pull_request" && action == "closed" && pull_request.merged == true`, then matches the Watchfire task by `pull_request.head.ref == watchfire/` and calls `task.MarkDoneIfNotAlready` + emits a Pulse `RUN_COMPLETE` notification titled ` — PR # merged`. Closes the v4.0 Beacon "Known issue" #1.
- **Slack slash-command HTTP transport — closes the v4.0 Beacon Slack-parity gap.** New `internal/daemon/echo/handler_slack_commands.go` translates the URL-encoded slash-command form body (`command`, `text`, `team_id`, `channel_id`, `user_id`, `trigger_id`) into a call against the shared transport-agnostic `commands.Route(...)` router, then renders `CommandResponse` as Slack response JSON (`{response_type: "in_channel" | "ephemeral", text, blocks}`). `/watchfire status / retry / cancel` now works in Slack at parity with the Discord interactions endpoint that shipped in Beacon. Closes the v4.0 Beacon "Known issue" #2.
- **OAuth bot tokens for Slack and Discord.** Replaces the v4.0 paste-a-signing-secret model with a proper OAuth install flow. Slack: `xoxb-...` bot token from the workspace OAuth callback, used for `chat.postMessage` so slash responses can include rich attachments and DM the originator on private failures. Discord: `Authorization: Bot ` for inbound auth and command registration. New "Connect Slack" / "Connect Discord" buttons in the Integrations settings UI launch the flow in the user's default browser; success surfaces a `Connected as ` pill. The legacy signing-secret + public-key path stays additive for users mid-cutover.
- **GitHub Enterprise / GitLab / Bitbucket inbound parity.** Per-project `github_host` field on `models.InboundConfig` lets the existing GitHub HMAC-SHA256 verifier target arbitrary GitHub Enterprise hostnames. New `internal/daemon/echo/handler_gitlab.go` verifies `X-Gitlab-Token` (per-project shared secret), narrows on `Merge Request Hook` events with `action: merge`. New `internal/daemon/echo/handler_bitbucket.go` verifies `X-Hub-Signature` (HMAC-SHA256), narrows on `pullrequest:fulfilled` events. Settings UI surfaces a "Git host" picker on inbound config.
- **Per-IP rate limiting on the inbound HTTP server.** Per-IP token bucket via `golang.org/x/time/rate`, default 30 req/min/IP across every `/echo/*` route, configurable through `models.InboundConfig.RateLimitPerMin` (`0` disables). Idempotent deliveries already in the LRU cache do NOT count against the bucket. On 429, the daemon logs a single WARN per IP per minute to avoid log flooding under sustained traffic.
- **Slack interactive components — buttons + cancel-reason modal.** The Slack outbound `TASK_FAILED` Block Kit template gains three action buttons: `Retry`, `Cancel`, `View in Watchfire`. New inbound endpoint `POST /echo/slack/interactivity` handles the `block_actions` and `view_submission` payloads with the same v0 HMAC verification + 5-minute drift window as the slash-commands endpoint. Button presses route through `commands.Route` so a `Retry` click is the exact equivalent of `/watchfire retry`. `Cancel` opens a Slack modal that asks "Why are you cancelling?"; the supplied reason lands in `task.failure_reason`.
- **Discord slash-command auto-registration on guild join.** The daemon now enumerates the guilds the bot is in at startup and POSTs the three slash-command schemas to each via the existing `internal/cli/integrations_discord.go::registerForGuild` helper; it also subscribes to `GUILD_CREATE` Gateway events so a freshly-added guild gets commands within 30 seconds (no CLI step). The Settings UI lists every guild with a ✓ / ✗ registration pill. The manual `watchfire integrations register-discord ` CLI stays as a fallback. Discord's commands API is upsert-style, so re-running is safe.
- **Settings UI: macOS-style category sub-pages with search.** Both GUI (`gui/src/renderer/src/views/Settings/GlobalSettings.tsx`) and TUI (`internal/tui/settings.go`) replace the single long scrolling page with a two-pane layout — left sidebar of eight categories (Appearance, Defaults, Agent Paths, Notifications, Integrations, Inbound, Updates, About), right pane shows only the selected category. New search input filters categories AND surfaces individual matching controls with category breadcrumbs; clicking a result navigates to the category and pulses the matching field for ~1.5s. GUI: `Cmd/Ctrl+F` focuses search, `Esc` clears, `Up/Down/Enter` navigate. TUI: `/` opens a search overlay with the same field-jumping behaviour. Deep-link routes (`#integrations` etc.) still work.

### Fixed

- **Run-all silently halted on auto-merge failure.** When `internal/daemon/agent/taskdone.go::HandleTaskDone`'s silent merge failed (dirty `main`, merge conflict, post-merge hook failure), the chain stopped — but **silently**: the task YAML still showed `status: done` + `success: true`, no notification fired, and the user was left wondering why their queue stalled. `onTaskDoneFn` now returns a structured `TaskDoneResult{Outcome, Reason}` (with `TaskDoneOK` / `TaskDoneMergeFailed` / `TaskDoneCancelled`) instead of a bare bool; `monitorProcess` branches on `result.Outcome == TaskDoneMergeFailed` and emits a `TASK_FAILED`-shaped notification before the chain decision; `runSilentMerge` populates the task's new `merge_failure_reason` field (`yaml: merge_failure_reason,omitempty`, exposed via proto + GUI/TUI). The chain-stop semantics are unchanged — the user still has to clean up `main` manually — but the silence is gone.
- **GUI chat-tab repainted multiple times on project switch.** Locked in single-mount + single-start guards in `gui/src/renderer/src/views/ProjectView/RightPanel/ChatTab.tsx`: the auto-start `useEffect` deps tightened to `[!!agentStatus, isRunning, projectId]` so a stale `agentStatus` reference from the previous project no longer fires `handleStart` on a transient render edge; the `autoStarted.current = false` reset on `projectId` change runs before the auto-start check. Regression test simulates rapid project switching and asserts `handleStart` fires exactly once per navigation.
- **Failed-task UI hid the reason behind two clicks.** `TaskStatusBadge` now carries a `title=` tooltip for agent-reported failures (it already had one for merge failures only), populated by a new exported pure helper `computeBadgeTooltip` that prefers `Merge failed: …` over `Failed: …` when both reasons are set and truncates to 500 runes. `TaskItem` passes `failureReason={task.failureReason}` into the badge alongside `mergeFailureReason`. `TaskModal`'s tab decision is now lazy in `useState(() => …)` AND kept in sync via the existing effect, so `done` tasks land on the Inspect tab on first paint without a flicker. The TUI task list (`internal/tui/tasklist.go`) renders an inline preview of both reasons (merge-failure precedence) under the `[✗]` glyph.

### Tests

- **Inbound framework coverage gap closed.** Filled out `internal/daemon/echo/`'s test surface — every signature verifier (GitHub HMAC-SHA256, Slack v0, Discord Ed25519) covers golden-path + every rejection mode (missing header, malformed signature, drift overshoot, replay window); `idempotency.go`'s LRU+TTL behaves correctly under concurrent access, eviction, and TTL refresh; `commands.Route` round-trips `status` / `retry ` / `cancel ` against a mocked task manager.

### Migration

- All Flare features are additive — projects upgrade with no behaviour change.
- Inbound: existing signing-secret + public-key configs continue to work; OAuth is opt-in via the new "Connect Slack" / "Connect Discord" buttons. The new `RateLimitPerMin` field defaults to 30; set to 0 to disable.
- Multi-host inbound: leave `github_host` empty for github.com; set per-project for GitHub Enterprise. GitLab and Bitbucket handlers are inactive until their per-project secret is configured.
- Discord auto-registration runs on next daemon start — existing guilds get re-upserted (idempotent). The CLI `watchfire integrations register-discord ` stays available as a fallback.
- Run-all halt fix: `onTaskDoneFn`'s signature changed from `func(...) bool` to `func(...) TaskDoneResult`. Internal callback only — no external API impact, but third-party forks pinning to the old signature will need to update.