Status: implemented MVP Scope: GitHub.com pull-request comments backed by an existing T3 thread and worktree
An authorized GitHub user can mention the T3 GitHub App in a pull-request conversation and continue the T3 thread whose checked-out worktree branch resolves to that PR.
The GitHub entry point joins or provisions:
- Work-item store — unique thread for this PR URL, or unique thread for a Jira key in the PR title or mention comment (Jira/Discord-first sessions continue).
- Live worktree match — unique checked-out PR worktree on a matching T3 project.
- Provision — prepare a PR worktree and create a T3 thread when nothing unique matches.
On accept, the bridge appends the PR URL and Jira keys extracted from the PR title and the mention
comment body to ThreadWorkItemStore. It never creates a project. See
work-item thread linking.
Setup and development webhook instructions are in
GitHub App setup. For agent gh/git auth without a machine user, see
GitHub App agent auth. The longer in-process broker plan is
GitHub App account migration.
The bridge handles two GitHub comment surfaces on pull requests:
| Surface | Webhook event | Where you write | Where T3 replies |
|---|---|---|---|
| PR conversation | issue_comment.created |
Conversation tab | New conversation comment |
| Inline review | pull_request_review_comment.created |
Files changed (line comment) | Reply in that review thread |
A turn starts only when a non-bot user writes an explicit configured mention followed by a prompt:
@t3-code investigate why the Windows check is failing
On an inline review comment, the same mention works and the agent receives the file path, line, side, and diff hunk from the review comment:
@t3-code please fix this null check
Both surfaces share the PR worktree (the same checkout Discord/T3 use for that PR). Sessions differ:
| Surface | Default | Behavior |
|---|---|---|
| Conversation | main | Reuse the live T3/Discord PR work thread |
| Inline review | sibling | First @mention in a GH review discussion creates a T3 session; later @mentions in that same discussion reuse it |
Overrides (stripped from the prompt):
| Flag | Effect |
|---|---|
main-thread / --thread main |
Force the PR work thread |
sibling-thread / --thread sibling / --thread new |
Force a new sibling session (and rebind that GH discussion to it) |
So: new T3 thread only when starting an unbound inline discussion, or when forced. Continuing a tagged review thread stays on the assigned session until the user switches.
The app:
- Verifies the webhook signature and deduplicates the delivery.
- Checks the repository allowlist and the requester's current repository permission.
- Resolves session: main PR thread, existing review-discussion binding, or create sibling on the PR worktree.
- Acknowledges the source comment with an eyes reaction, starts a turn, and posts the final answer (conversation comment or in-thread review reply).
If the chosen T3 thread is already running, the app asks the user to retry instead of queuing. Edited comments, mention-free comments, normal issue comments (non-PR), and app-authored comments are ignored.
When a GitHub invocation successfully resolves a T3 thread, the server records the PR URL in the
shared ThreadWorkItemStore (stateDir/thread-work-items.json) alongside any Jira issue keys.
That store is platform-agnostic (not Discord-only) and can later support reverse lookup, UI, and
agent tools without reading Discord bot state.
Live PR resolution below remains the primary GitHub link path for this MVP.
A PR is linked only while exactly one active T3 thread satisfies all of these conditions:
worktreePathis non-null.branchis non-null.- Git can inspect the worktree.
- T3's source-control provider resolves the branch to a GitHub PR.
- The resolved PR number and canonical URL match the webhook repository and PR.
This is a live PR/branch/worktree/thread relationship rather than a second mutable link database. It supports both directions already present in T3:
- A PR checked out through
git.preparePullRequestThreadhas its upstream configured and resolves to that PR. - A PR created from an existing T3 worktree branch resolves through the branch's GitHub PR status.
Deleting the worktree, removing the thread, switching the branch to another PR, or producing multiple matching threads immediately makes the lookup fail closed. The webhook never attempts repair.
GitHub App issue_comment | pull_request_review_comment webhook
|
v
POST /api/github/webhook
raw-body HMAC verification
payload/schema validation
delivery-id dedupe
|
v
GitHubPrBridge
repository allowlist
actor permission lookup
parse thread mode (conversation→main, review→sibling+affinity; flags override)
resolve: main PR thread | bound review-discussion session | create sibling
|
v
OrchestrationEngineService.dispatch(thread.turn.start)
(inline review: path/line/diff hunk in prompt context)
|
v
ProjectionSnapshotQuery polling
--> issue surface: create/update Issues API comment
--> review surface: create/update review-thread reply
Implementation lives under apps/server/src/github/. It runs inside the T3 server so it uses the same
orchestration projection and command engine as the web application.
- Verify
X-Hub-Signature-256against the exact raw request body before decoding JSON. - Accept at most 1 MiB per webhook body.
- Accept only
issue_commentandpull_request_review_commentevents with a non-emptyX-GitHub-Deliveryid. - Ignore bot actors and require an explicit configured mention.
- Require the repository to be enabled and the actor to meet the configured permission floor
(
writeby default). - Treat all GitHub fields and comment text as untrusted user input in the generated T3 prompt.
- Use a GitHub App installation token for permission checks and PR comment writes.
- Keep the webhook secret and private key out of prompts, logs, persisted deliveries, and git config.
- Use the checked-out branch's provider-resolved canonical PR URL; branch-name equality alone is never sufficient.
Private repositories should set T3CODE_GITHUB_ALLOWED_REPOSITORIES; an empty value allows every
repository on which the app is installed.
Processed deliveries are persisted atomically in:
${T3CODE_HOME}/userdata/github-webhook-deliveries.json
Inbound webhook intake is also logged (24h retention) to:
${T3CODE_HOME}/userdata/github-webhook-debug.ndjson
Each row records outcome (accepted_202, ignored_202, invalid_400, unauthorized_401,
repo_denied_202, …), delivery id, and a capped body preview on failures. Best-effort only.
Development mode uses the corresponding dev state directory. The newest 2,000 deliveries are kept. Claiming a delivery is serialized, so a GitHub retry cannot create a second T3 turn. Each record stores the response comment, T3 thread, and previous turn id.
On restart, processing records resume projection polling and finalize the original GitHub comment. Installation tokens are cached briefly and renewed automatically. A response longer than GitHub's comment limit is truncated explicitly.
Temporary GitHub/T3 failures are logged and do not get mislabeled as an unlinked PR. A missing thread, missing worktree, failed branch resolution, repository mismatch, PR mismatch, or ambiguous match does.
| Variable | Required | Default | Purpose |
|---|---|---|---|
T3CODE_GITHUB_APP_ID |
yes | — | Numeric GitHub App id |
T3CODE_GITHUB_APP_PRIVATE_KEY_PATH |
yes | — | Path to the downloaded PEM key |
T3CODE_GITHUB_WEBHOOK_SECRET |
yes | — | Shared webhook HMAC secret |
T3CODE_GITHUB_APP_MENTION |
yes | — | Mention handle without @ |
T3CODE_GITHUB_ALLOWED_REPOSITORIES |
no | all installed repos | Comma-separated owner/repo allowlist |
T3CODE_GITHUB_MIN_PERMISSION |
no | write |
read, triage, write, maintain, or admin |
T3CODE_GITHUB_TURN_TIMEOUT_MS |
no | 1800000 |
Response bridge timeout, minimum 10 seconds |
The route returns 404 unless all four required variables are configured.
| Condition | Result |
|---|---|
| No unique live PR/branch/worktree/thread match | Exactly not yet linked/checked out. |
| Missing/deleted worktree or T3 thread | Exactly not yet linked/checked out. |
| Repository or PR mismatch | Exactly not yet linked/checked out. |
| Unauthorized repository | Silently ignored; no response, no turn |
| Unauthorized actor | Neutral authorization response; no link-state disclosure |
| Thread already running | Busy response; no queue and no turn |
| Duplicate delivery | Reuse persisted classification; no new comment or turn |
| Turn completes | Replace working/progress comment with final answer |
| Turn errors or is interrupted | Replace comment with a stable failure response |
| Server restarts during turn | Resume the persisted response bridge |
Automated coverage includes raw-body signatures, invocation parsing, bot/issue/empty-prompt ignores, permission ordering, GitHub App JWT signing, and the exact missing-link response.
End-to-end acceptance:
- Check out a GitHub PR into a T3 worktree-backed thread.
- Mention the app with a prompt on that PR conversation.
- Confirm exactly one new user turn appears in the same T3 thread and its final answer is posted as a conversation comment.
- Mention the app on an inline Files-changed review comment on the same PR.
- Confirm the reply lands in that review thread and the turn prompt includes path/line context.
- Redeliver the same webhook and confirm no duplicate comment or turn appears.
- Approval and structured user-input interactions in GitHub.
- GitHub Checks output.
- Durable relay ingress for environments that cannot expose the local server directly.
- Replacing the source-control implementation's personal
ghand git credentials with GitHub App installation credentials; see the migration plan linked above.