|
| 1 | +# Frame: Review notifications |
| 2 | + |
| 3 | +- **Date:** 2026-07-13 |
| 4 | +- **Alignment:** design-docs/02-review-notifications.alignment.md |
| 5 | + |
| 6 | +Naming fixed by this frame (per glossary): tools `subscribe_change` / `unsubscribe_change`; flag family |
| 7 | +`--review-notifications` (enable), `--review-notifications-poll-interval`, `--review-notifications-include-own`, |
| 8 | +`--review-notifications-exclude-accounts`, `--review-notifications-exclude-patterns`, each with the matching |
| 9 | +`GERRIT_MCP_REVIEW_NOTIFICATIONS*` mirror. The `--notify-*` prefix is deliberately avoided — `notify` already means |
| 10 | +email-notify levels on review posting. New package: `internal/notifications` (subscription store, poller, filters, |
| 11 | +payload rendering); transport wrapper lives beside the server assembly. |
| 12 | + |
| 13 | +## Phase 1: Tracer Bullet — subscribe, poll, push |
| 14 | + |
| 15 | +The thinnest wire through every layer: one flag, one tool, a poller that detects only `updated` movement, a minimal |
| 16 | +payload, and a channel notification that reaches a real client. |
| 17 | + |
| 18 | +**Components:** |
| 19 | + |
| 20 | +- `internal/config` — `--review-notifications` behaviorFlag (+ mirror); interval flag with 60s fallback. |
| 21 | +- `internal/notifications` — subscription store (mutex-guarded map keyed by change number, per-change cursor = |
| 22 | + last-seen `updated`); poller goroutine: batched `change:A OR change:B` query per tick, emits a bare event when |
| 23 | + `updated` moved. |
| 24 | +- `internal/tools` — `subscribe_change` tool (validates the change exists and is in project scope via |
| 25 | + `GetChange`, stores it, returns llmxml ack naming current status and patch set). |
| 26 | +- `cmd/go-gerrit-mcp` — capability declaration (`Experimental["claude/channel"]`) and conditional instructions |
| 27 | + sentence when enabled; wrapping transport capturing the `mcp.Connection`; poller lifecycle bound to the signal |
| 28 | + context. |
| 29 | +- Emission — ID-less `jsonrpc.Request`, method `notifications/claude/channel`, params `{content, meta}`; content is |
| 30 | + a minimal `<review_activity change="..." status="...">` element for now. |
| 31 | + |
| 32 | +**Testing strategy:** |
| 33 | + |
| 34 | +- Learning tests first (see Learning Tests) — SDK capability override and raw-write behavior are assumptions until |
| 35 | + executed. |
| 36 | +- Unit: store add/duplicate/remove; poller tick against `httptest` Gerrit stub (updated moved / not moved / query |
| 37 | + error → logged and retried). |
| 38 | +- Manual probe: JSON-RPC pipe with a subscription, mutate the change live, observe the notification line on stdout. |
| 39 | + |
| 40 | +**Verification gate:** |
| 41 | + |
| 42 | +- Live end-to-end against Claude Code: `claude --dangerously-load-development-channels server:gerrit`, subscribe to |
| 43 | + a sandbox change, post a comment on it from the UI, the `<channel source="gerrit">` block appears in the session. |
| 44 | + This gate validates the entire contract stack before any depth is built. |
| 45 | + |
| 46 | +**Acceptance criteria:** |
| 47 | + |
| 48 | +- [ ] Zero-config server is byte-identical to today: no tool, no capability, no instructions change, no goroutine. |
| 49 | +- [ ] With the flag on, subscribing then mutating the change produces exactly one notification per poll tick with |
| 50 | + movement; quiet ticks produce nothing; empty subscription set skips the query entirely. |
| 51 | + |
| 52 | +## Phase 2: Real activity deltas and terminal states |
| 53 | + |
| 54 | +**Components:** |
| 55 | + |
| 56 | +- `internal/notifications` — cursor grows per-kind high-water marks; event extraction from `GetChange` detail + |
| 57 | + `ListChangeComments`: new change messages (author, date, tag, revision), new votes (`ApprovalInfo.Date` newer |
| 58 | + than cursor, value, label), new/updated comment threads (reuse the thread pipeline), status transitions. |
| 59 | +- `internal/tools` — `unsubscribe_change` tool; payload rendering: `<review_activity>` composing existing |
| 60 | + vocabulary — `<message>`, `<vote>`, thread/`<comment>` elements exactly as `get_change_comments` renders them; |
| 61 | + `meta` carries `change`, `kind`, `project`. |
| 62 | +- Terminal handling — merged/abandoned detected from status: final notification carries the transition plus |
| 63 | + `subscription="ended"` semantics in prose, change leaves the store. |
| 64 | + |
| 65 | +**Testing strategy:** |
| 66 | + |
| 67 | +- Golden tests for payload rendering (existing golden harness in `internal/tools`). |
| 68 | +- Unit: cursor semantics — replay the same poll twice, second emits nothing; vote-only update emits a vote event; |
| 69 | + comment burst groups into one payload. |
| 70 | +- Live probe against the production incident change for thread-render parity. |
| 71 | + |
| 72 | +**Verification gate:** |
| 73 | + |
| 74 | +- A subscribed change taken through comment, reply, vote, and submit on a sandbox produces the expected |
| 75 | + notification sequence ending in the auto-unsubscribe notice, and the store is empty afterwards. |
| 76 | + |
| 77 | +**Acceptance criteria:** |
| 78 | + |
| 79 | +- [ ] Every activity kind (message, vote, thread, transition) renders and pushes; nothing repeats across ticks. |
| 80 | +- [ ] Terminal states always end the subscription with the announcing notification, including when the terminal |
| 81 | + transition and other activity arrive in the same tick. |
| 82 | + |
| 83 | +## Phase 3: Filters and model-facing prompts |
| 84 | + |
| 85 | +**Components:** |
| 86 | + |
| 87 | +- `internal/config` — `include-own`, `exclude-accounts`, `exclude-patterns` flags; regex compilation at load with |
| 88 | + aggregated fail-loud errors. |
| 89 | +- `internal/notifications` — filter chain applied per extracted event: self-authorship (default drop, flag keeps), |
| 90 | + excluded accounts (username or numeric ID), content regexes against message/comment text. Filtered-to-empty |
| 91 | + ticks emit nothing. |
| 92 | +- `internal/tools` / `cmd` — full instructions section: when to subscribe, what arrives, auto-unsubscribe meaning, |
| 93 | + re-subscribe-after-restart guidance; tool descriptions final. |
| 94 | + |
| 95 | +**Testing strategy:** |
| 96 | + |
| 97 | +- Unit: table tests per filter and their composition; config tests for invalid regex (startup fails naming the |
| 98 | + pattern) and mirror resolution. |
| 99 | +- Golden: instructions output with the feature on/off. |
| 100 | + |
| 101 | +**Verification gate:** |
| 102 | + |
| 103 | +- Live: a bot-authored comment matching an exclusion pattern produces no notification while a human reply in the |
| 104 | + same tick does. |
| 105 | + |
| 106 | +**Acceptance criteria:** |
| 107 | + |
| 108 | +- [ ] Own activity silent by default, delivered with the include flag. |
| 109 | +- [ ] Account and pattern exclusions drop matching events only; invalid regex aborts startup with an aggregated |
| 110 | + error naming the pattern. |
| 111 | + |
| 112 | +## Phase 4: Hardening and operator docs |
| 113 | + |
| 114 | +**Components:** |
| 115 | + |
| 116 | +- `internal/notifications` — poll failure policy (log, keep subscription, retry next tick; repeated failures do not |
| 117 | + kill the poller), change-became-inaccessible handling (scope loss / deletion → end subscription with notice), |
| 118 | + clean shutdown ordering on context cancellation. |
| 119 | +- `README.md` — feature section: flags table rows, channel enablement (`--channels` / research-preview |
| 120 | + `--dangerously-load-development-channels server:<name>`), org-policy caveat, per-project configuration synergy. |
| 121 | +- `docs/` — glossary already updated; verify vocabulary consistency across tool descriptions and README. |
| 122 | + |
| 123 | +**Testing strategy:** |
| 124 | + |
| 125 | +- Unit: error-path tests (Gerrit 5xx, 404 on subscribed change, ctx cancellation mid-tick). |
| 126 | +- `go test -race` across the package (first concurrent component in the codebase). |
| 127 | + |
| 128 | +**Verification gate:** |
| 129 | + |
| 130 | +- Kill/restart and failure-injection probes leave no goroutine leaks (`-race` clean, poller exits with the session). |
| 131 | + |
| 132 | +**Acceptance criteria:** |
| 133 | + |
| 134 | +- [ ] Poller survives transient Gerrit failures and shuts down cleanly with the session. |
| 135 | +- [ ] README documents the feature end-to-end including research-preview caveats. |
| 136 | + |
| 137 | +## Learning Tests |
| 138 | + |
| 139 | +- **SDK capabilities override** — setting `ServerOptions.Capabilities` with `Experimental["claude/channel"]` |
| 140 | + preserves the inferred tools capability (research flags the interaction; verify before Phase 1 builds on it). |
| 141 | +- **Raw notification write** — an ID-less `jsonrpc.Request` written via the wrapped `mcp.Connection` concurrently |
| 142 | + with SDK traffic is framed correctly and received by a client (probe with a stub client before trusting it). |
| 143 | +- **Channel injection** — Claude Code with the development flag renders our notification as a `<channel>` block |
| 144 | + (Phase 1 gate doubles as this test). |
| 145 | +- **Gerrit vote messages** — a bare vote on the production instance produces a `ChangeMessageInfo` and a dated |
| 146 | + `ApprovalInfo`; default query results include `updated` without `o=` options. |
| 147 | + |
| 148 | +## Phase Sequence |
| 149 | + |
| 150 | +Phase 1 (tracer bullet, no deps) |
| 151 | + ↓ |
| 152 | +Phase 2 (depends on Phase 1) |
| 153 | +Phase 3 (config/filter work can start against Phase 1; filter-chain integration depends on Phase 2's event model) |
| 154 | + ↓ |
| 155 | +Phase 4 (depends on Phases 2–3) |
| 156 | + |
| 157 | +## Scope Boundaries |
| 158 | + |
| 159 | +**In scope:** subscription tools, poller, filters, payload rendering, channel emission, operator docs. |
| 160 | +**Out of scope:** multiple notification channels and per-channel configuration; subscription persistence; |
| 161 | +attention-set/stream-events/webhook sources; permission relay; official channel-plugin packaging. |
0 commit comments