GitHub App webhook service that performs automated pull request reviews using native Octokit REST tools (src/agent/githubTools.ts) and @earendil-works/pi-ai (LLM + tool loop).
Durable agent work (Postgres intake + pg-boss workers) is described in docs/adr/0009-durable-agent-work.md. Operations runbooks: docs/agent-work-ops.md. Domain terms: CONTEXT.md. All tunables: docs/configuration.md.
- On
pull_request(opened,synchronize,reopened), enqueues an automated general review. A worker adds 👀 (eyes) on the PR issue, posts a progress stub, runs an agent loop, and upserts## PR Agent Reviewon the PR conversation when the model succeeds. A pull request review on the Files tab (with inline P0–P2 threads) is posted only when those severities are present; its review pointer body includes a collapsible agent fix prompt aggregating all findings for copy-paste into coding agents. - On
issue_commentandpull_request_review_comment(createdonly), detects/help,/ask,/review,/review-security, and/review-quality, enqueues work, and routes commands. Reactions and replies are published by workers, not on the webhook fiber. - Responds
200after durable intake commits to Postgres and pg-boss jobs are enqueued (or503if intake cannot commit — GitHub may redeliver). Reactions, progress comments, reviews, and ask answers run inROLE=workerand may appear seconds after the HTTP response. The webhook does not wait for LLM runs to finish.
- Payload boundary: each subscribed
X-GitHub-Eventtype is validated with minimal Zod shapes before deduplication; malformed payloads are logged and skipped without inserting a dedupe row (so GitHub retries can succeed after fixes or transient issues). - Slash commands are detected on the first non-empty line only, and are case-sensitive (
/reviewworks;/Reviewdoes not)./ask <question>answers one question about the PR or a specific diff line. - Webhook deduplication is durable:
webhook_events.dedupe_keyusesX-GitHub-Deliverywhen present, otherwise SHA-256(raw body). Duplicate deliveries return200without creating duplicate work items. - Auto-review superseding: on
pull_requestsynchronize, newer automated general-lens work supersedes queued auto-reviews for the same PR and requests cooperative cancel on an in-flight auto-review (slash-command reviews are not superseded). - Agent loop (reviews): capped at
MAX_TOOL_ROUNDS(tools required on the first round). Each run is a single-pass review: one investigation sweep, then onesubmitReviewwith all evidenced P0–P2 findings (unlimited count; per-field and summary body size limits apply). IfsubmitReviewnever succeeds, publish-recovery nudges run up toMAX_REVIEW_PUBLISH_ATTEMPTS, then a plain-text fallback comment may be posted when structured publish is exhausted. - Review pointer link: on the second and later review runs per PR and lens, the Files-tab pointer links to the existing PR conversation summary comment when it can be verified; the first completed summary for that lens uses plain text only.
- Worker concurrency: review, ask, and acknowledgement jobs are capped per process by
REVIEW_CONCURRENCY(default2),ASK_CONCURRENCY(default1), andACK_CONCURRENCY(default2) via pg-boss workerlocalConcurrency(src/agentWork/worker.ts). Multi-replica deployments remain at-least-once at the worker layer. - GitHub tools: 11 investigation tools in
src/agent/githubTools.ts, plus 2 Context7 doc tools; the server publishes only viasubmitReview(no agent-callable comment/review delivery tools). See docs/adr/0004-native-pi-ai-toolset.md. - Library docs lookup: review and ask agents get two Context7 tools (
resolveLibraryId,getLibraryDocs) that hithttps://context7.com/apito verify upstream API claims. Anonymous calls work for public libraries with rate limits; setCONTEXT7_API_KEYfor higher limits and private repos. See docs/adr/0003-context7-docs-tool.md. - Cursor provider: set
PI_PROVIDER=cursor,CURSOR_API_KEY, andPI_MODEL(e.g.composer-2.5). Worker registers pi-ai apicursor-sdkand runs Cursor local agents with an HTTP MCP bridge to pr-agent's GitHub/Context7/submitReview tools. See docs/adr/0013-cursor-sdk-provider.md. - Bot identity for self-suppression is cached per
GITHUB_APP_ID, so multiple GitHub Apps in one process do not share the same cache entry. WEBHOOK_TIMEOUT_MS(default10000) is a logging-only budget on webhook intake duration; it does not cancel worker jobs./review-security— trigger-only deep security review (DeepSec-adapted prompt; see NOTICES.md). Never runs onpull_requestwebhooks. Uses the same review worker lane andMAX_TOOL_ROUNDSas/review; large PRs may need a higherMAX_TOOL_ROUNDS. Posts a separate summary comment (## PR Agent Security Review) that can coexist with the general review summary./review-quality— trigger-only deep code-quality review (thermo-nuclear-adapted prompt; see NOTICES.md and docs/adr/0016-review-quality-lens.md). Never runs onpull_requestwebhooks. Uses the same review worker lane andMAX_TOOL_ROUNDSas/review. Posts a separate summary comment (## PR Agent Quality Review) focused on maintainability and structural simplification; can coexist with general and security summaries./ask— interactive Q&A about PR code (PR conversation or inline diff comment). Runs on theagent-work-askpg-boss queue withASK_CONCURRENCY(default1),MAX_ASK_TOOL_ROUNDS(default12), andMAX_ASK_FINALIZE_ROUNDS(default2) for extra model turns when the tool loop ends on tool results. Inline replies are plain text; PR conversation replies repeat the question in a short wrapper. See docs/adr/0008-ask-command.md.
@octokit/plugin-throttlingpaces all installation-token REST calls (review tools, publish, reactions). Tune via env:MAX_PR_FILES_LISTED(default300),MAX_PR_FILES_PATCH_BYTES(default500000).- On tool failures, logs emit
github_tool_request_errorwithx-github-request-id,x-ratelimit-*, and a classification — capture a redacted sample when debugging production limits. - See docs/adr/0007-github-api-rate-limits.md for policy (secondary-limit retries, circuit breaker, truncation trade-offs).
- Create a GitHub App; set Webhook URL to
https://<host>/webhooksand Webhook secret →WEBHOOK_SECRET. - Subscribe to events:
pull_request,issue_comment,pull_request_review_comment(do not requirepull_request_reviewfor v1). - Repository permissions (typical): Issues and Pull requests read/write (reactions + comments + reviews), Contents read, Metadata read. Tighten further if you fork this code to only the REST calls you need.
- Install the app on target org/repos; note the App ID and generate a private key for
GITHUB_APP_ID/GITHUB_APP_PRIVATE_KEY.
DATABASE_URL is required for both ROLE=web and ROLE=worker (src/config.ts).
docker compose up postgres # or: docker compose up for the full stack
cp .env.example .env # GITHUB_*, WEBHOOK_SECRET, DATABASE_URL, provider keys — see docs/configuration.md
corepack enable # Node 22+ ships Corepack; activates pnpm from package.json
pnpm install
# terminal 1 — webhooks only enqueue work
ROLE=web DATABASE_URL=postgres://pr_agent:pr_agent@localhost:5432/pr_agent pnpm dev
# terminal 2 — reactions, reviews, asks
ROLE=worker DATABASE_URL=postgres://pr_agent:pr_agent@localhost:5432/pr_agent pnpm devpnpm dev with ROLE=web alone accepts webhooks but does not run reviews or asks without a worker. See docs/agent-work-ops.md for queue inspection and recovery.
Tunnel webhooks (e.g. smee.io) to your local PORT, then point the GitHub App webhook at the smee URL forwarding to /webhooks.
- Production boot is Effect TS with a web/worker split (
ROLEenv). - Web:
processWebhookRequestEffect→WebhookDispatcher→WebhookHandlers+AgentWorkScheduler(Postgres + pg-boss enqueue). - Worker:
AgentWorkerRuntimeLiveconsumes acknowledgement, review, and ask queues; PR-surface I/O and LLM runs happen on worker fibers. - Maintainer rules for tunables: AGENTS.md.
pnpm run check:effect-versionsenforces pinned versions:effect@3.21.2@effect/platform@0.96.1@effect/platform-node@0.106.0
pnpm testruns this version gate before Vitest (pretest).
- Stack: docker-compose.yml runs
postgres,pr-agent-web(ROLE=web), andpr-agent-worker(ROLE=worker).docker compose upis required for end-to-end reviews and asks; web-only is not sufficient. - Image: multi-stage
Dockerfile(Node 22); runtime listens onPORT(pinned to 7224 in Compose and.env.example). - Health:
GET /healthreturns200and plainok(used byHEALTHCHECKin the image and by Compose). - Webhook URL (when Compose maps default ports):
http://<host>:7224/webhooks— same path as bare Node. DATABASE_URLis set in Compose for both app services (postgres://pr_agent:pr_agent@postgres:5432/pr_agent).- Provider API keys (for example
OPENAI_API_KEYorCURSOR_API_KEYwhenPI_PROVIDER=cursor) are not fully read bysrc/config.tsexceptCURSOR_API_KEYwhen the Cursor provider is selected; other Pi AI secrets load from the environment. Set them in.envbeside the GitHub fields or reviews fail at runtime in the worker. - Secrets: never commit
.env; keep Compose files off public pastebins.
cp .env.example .env
# Set real GITHUB_*, WEBHOOK_SECRET, DATABASE_URL (if not using Compose defaults), and OPENAI_API_KEY (or keys for your PI_PROVIDER)
docker compose build
docker compose upCompose sets environment.PORT=7224 and 7224:7224 publishing so host and container ports match. For a host port clash, change ports to for example 7227:7224 and keep container PORT at 7224.
Requires Docker Engine with Compose v2 (CLI plugin). env_file defaults to .env; use host env PR_AGENT_ENV_FILE for an alternate path (variable substitution in the Compose file).
Alternate env file path (CI or smoke):
PR_AGENT_ENV_FILE=/abs/path/to/.env docker compose up| Script | Purpose |
|---|---|
pnpm dev |
Run src/index.ts (ROLE env) |
pnpm build |
Compile to dist/ |
pnpm start |
Run compiled dist/ |
pnpm typecheck |
tsc --noEmit (src/ only) |
pnpm lint |
Type-aware Oxlint |
pnpm lint:fix |
Oxlint with safe fixes |
pnpm fmt |
Format with Oxfmt |
pnpm fmt:check |
Check formatting |
pnpm check:code |
typecheck + lint + fmt:check |
pnpm run check:effect-versions |
Verify pinned Effect deps |
pnpm test |
Vitest (test/**/*.test.ts) |
pnpm test:watch |
Vitest watch mode |
Type-aware lint requires oxlint-tsgolint (dev dependency). pnpm-workspace.yaml sets minimumReleaseAge: 10080 (7 days) for registry installs; pg-cloudflare is excluded as a fresh transitive dependency of pg.
- Treat
WEBHOOK_SECRETand app private keys as production secrets. /askapplies deterministic outbound redaction (tokens, host URLs, PEM blocks) before posting replies; obvious bot-internals probes get an Ask meta refusal without an LLM call (ADR 0010). Review publish paths are unchanged.- Structured logging uses evlog with
service: pr-agent;LOG_LEVELmaps to evlogminLevel(defaultinfo). Atinfo, per-tool-round and rate-limit retry noise stays atdebugand is omitted from emitted wide events. LOG_MAX_WIDE_EVENTS(default128) caps sub-events per webhook/worker operation.LOG_PRETTYdefaults to off in production (JSON lines).- Production logging should stay at
infounless debugging a specific review run (LOG_LEVEL=debug).