Skip to content

PR conversations

A pull request is already where the conversation about code happens. PR conversations make it a surface on a Control Center workspace, the same way a chat bridge makes Slack one: @mention the server’s GitHub App bot on a PR, reply inside its review threads, or add the ai-review label, and the work happens in the PR’s review space — under the same membership, attribution and guardrails as anything typed in the app. The bot’s answers come back on GitHub, in the lane the question was asked in.

Three decisions shape the feature, and each is the same decision the chat bridge made for chat, restated for a forge: which direction the connection runs, what authorizes a comment that arrives from outside and who each side is attributed to.

GitHub’s only push channel for an app is a webhook, and a webhook needs an inbound URL — the one thing this server deliberately never requires. So discovery is a sweep: every minute or so, one aliased search per app installation asks GitHub for the open PRs that mention the bot or carry the review label, PRs that already have a review space get their comment threads read, and everything is diffed by comment id against what was seen last sweep.

The design consequences are worth naming:

  • Latency is a sweep interval, not an event. The 👀 acknowledgement lands within about a minute. Nothing urgent rides on it.
  • Exactly once, per comment. Comment ids are unique across GitHub; each is recorded the moment it is classified as eligible, before anything acts on it. A crash can lose a comment’s handling, never double-fire it.
  • Offline time is caught up, not replayed. The seen-set is persisted, so a restart delivers what arrived while it was down. The one exception is the first sweep ever: with no memory to diff against, everything outstanding is recorded silently — an operator who labeled twenty PRs before this server existed must not come back to twenty running reviews.
  • The label is a set, not an event. Search reports label presence, so a labeled PR triggers once, ever. Re-adding a label after removing it is invisible to the sweep; a mention is the re-run path.

The search runs once per installation account on that owner’s own client, because an installation token only sees its own installation’s repos — a single server-wide credential would quietly miss every PR under the app’s other installations.

A PR comment arrives carrying little more than an author login — an unauthenticated external identity until proven. The bot resolves it against a reverse index of the workspace’s members and their connected GitHub accounts, rebuilt on a TTL because people sign in and get invited while the server runs.

Failing closed looks like a ladder, and every rung answers in the thread it refused in:

  • An unlinked login is told how to link
  • A linked non-member is told membership is the boundary
  • A read-only member is told their role cannot start work

None of them proceed. A comment is never attributed to somebody who did not send it, and a stranger’s words never enter a workspace they cannot see.

Each side of the conversation carries a different identity, and the difference is the feature:

  • On GitHub, the bot is the server. Every comment, reaction and reply the bridge posts rides the app’s installation token. Nobody clicked anything in a client, so nothing borrows a person’s account — the same rule that keeps webhooks and polling off human credentials.
  • In the space, the question is yours. The message is attributed to the member the author resolved to, and the agent run executes on their behalf: commits co-author them, their stored credential backs the run, and the space’s autonomy dial and action guardrails apply exactly as they do to a message typed in the app.

Publishing review findings back to GitHub remains a person’s decision, made in Control Center. A mention can start a review; only a human publishes one.

Everything authored by a bot account is dropped before any gate runs — ours specifically, and every [bot] suffix, so another app’s review bot cannot wake ours either. Without this the bot’s own answers would arrive as new comments and re-trigger it forever.

The [bot] suffix is also why the short form exists. GitHub fixes an app account’s login at <slug>[bot], offers no autocomplete for it, and never resolves @slug to the app — so accepting the bare slug is our matcher’s decision, and discovering it needs a raw comment-text search lane rather than GitHub’s mention index. The cost is stated rather than smoothed: a human user who shares the app’s slug addresses the bot with every short-form mention of themselves. Distinctive app names make the collision hypothetical.

  • No auto-publish. Findings land in the review space; publishing to GitHub is a separate, human action
  • No fuzzy identity. The author resolves to exactly one member or is refused — there is no best-guess attribution
  • No inbound network. No webhook endpoint exists to secure, because no webhook endpoint exists