Behaviour semantics, deployment detail, and developer scripts for pr-agent. For env tunables see configuration.md. For queue SQL and recovery see agent-work-ops.md. Domain terms: CONTEXT.md.
- On
pull_request(opened,synchronize,reopened), durable intake runs. Onopened, it enqueues an automated orchestrated review run and a description run whenFEATURE_REVIEWorFEATURE_DESCRIBEisauto. Onsynchronize, it enqueues a verification run that re-checks open bot inline findings against the new head whenFEATURE_VERIFICATION=auto, and (whenFEATURE_REVIEW=auto) it cancels any auto review still in flight and enqueues one deferred-head replacement so the published review matches the new head.reopeneddoes not auto-enqueue work. Review workers add an acknowledgement 👀 reaction, post a review progress comment stub (Recon + four specialist rows from the first post), run the orchestrator and four specialists, publish inline findings in thread batches, and replace the stub with## PR Agent Reviewafter all specialists resolve. On terminal success the durable runner replaces 👀 with 👍; on terminal failure it replaces 👀 with 👎. Description workers merge generated content into the PR body inside<!-- PR_AGENT_DESCRIPTION_BEGIN -->/<!-- PR_AGENT_DESCRIPTION_END -->markers under## PR Agent Description, preserving user-authored text outside those markers, and use the same 👍 / 👎 outcome reactions. - On
issue_commentandpull_request_review_comment(createdonly), detects/help,/ask,/describe,/review,/cancel,/triage, and/verify. It also promotes allowed@botmentions to ask intake. Workers publish atomic lifecycle reactions (👀 → 👍 / 👎), replies, and reviews after durable intake; the webhook fiber does not perform PR-surface I/O. - 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.
Architecture: ADR 0006.
- Payload boundary: each subscribed
X-GitHub-Eventtype is validated with minimal Valibot shapes before deduplication. Malformed payloads are logged and skipped without inserting durable dedupe rows (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 (code anchor). - Webhook deduplication is durable:
webhook_events.dedupe_keykeeps delivery-ID correlation, whilewebhook_event_replays.body_sha256blocks the same verified body under any delivery ID. Both records useWEBHOOK_EVENTS_RETENTION_SECONDS(30 days by default); replay rows cascade when their event row expires. Duplicate deliveries return200without creating duplicate agent work items. Verified, parsed ignored events consume the same replay window; malformed payloads do not. - Review superseding: when a newer automated review is enqueued for the same PR, it supersedes queued auto-reviews and requests cooperative cancellation of an in-flight auto-review. A
synchronizepush does the same from intake: it supersedes queued auto-reviews, requests cancellation of the running one, and enqueues one deferred-head replacement (no replacement when no review is active, so pushes never re-review a finished PR). The running review observes the cancel request through a fast poll (REVIEW_CANCEL_POLL_INTERVAL_MS) while specialists run, and stops through the orchestrator's abort path, which aborts and joins every in-flight specialist. Automated verification onsynchronizeuses the same supersede pattern for prior verification items. Slash-command reviews are not superseded. - Review check-run identity: the worker binds
PR Agent Reviewto the exact repository, head SHA, check name, and requesting work-item external ID. It adopts a remote run only after a proven duplicate-creation error and one exact provider match; ambiguous or incomplete provider state remains unresolved. - Stale-head reschedule. Automatic-review preflight and the publish gate detect a moved PR head. The worker cancels the stale auto or slash run and enqueues one replacement review for the latest head. The replacement takes ownership of the existing review progress comment, so its acknowledgement refreshes the stub and later specialist ticks and summary update the same comment. A replacement that also goes stale fails with retry guidance instead of looping.
- Orchestrated reviews: the acknowledgement worker posts a queued progress stub (Head/Source, optional Queue
#N of Mwait rank among queued reviews for the same pull request, optional CI) before the review worker claims the item. The review orchestrator then performs reconnaissance and writes one specialist brief. The progress stub shows Recon as Running (specialists Waiting) until that brief is ready, then marks Recon Done and specialists Running. Correctness, security, quality, and tests specialists then run in parallel. Each completed report is judged once and may publish one incrementalCOMMENTreview. Specialist ticks use shared status copy (Waiting,Running,No findings,N findings,Failed);N findingscounts accepted ledger placements (inline plus summary-only), not inline threads alone. The final summary waits for all specialists. A failed specialist produces partial coverage, a neutral check run, and an error commit status. If all specialists fail, the worker posts a failure notice instead of a summary. - GitHub publish recovery: every non-idempotent review, ask, description, triage, verification, check, status, and label mutation records an operation intent before calling
PrSurface. A crash between GitHub acceptance and stashing__resultis resolved frompublish_recordsor exact provider evidence (operation-intent marker or provider id) and then treated as success. LeasedPrSurfacemutations use that same recover hook at the lease boundary. Reactions, labels, commit statuses, and finishing a check run cannot prove this attempt landed, so they stay fail-closed. Anoutcome_unknownintent is never remutated; automatic retry is allowed only when the provider proves that acceptance did not occur. If exact recovery is unavailable, the worker preserves the publish record and takes the bounded deterministic degradation path, so an accepted mutation is not duplicated. - Single active review: one review may be queued or running per pull request, enforced at execution time by the PR actor lease (
pr_actor_leases; review, description, triage, and verification each hold one lease per PR). A failed claim or payload load after acquisition stops renewal and releases that epoch so a later delivery can proceed; a failed SQL release waits for lease expiry. A duplicate/reviewis acknowledged but does not create another work item or change the active progress comment./review forceis the exception: it cancels the active run first (see below). /cancel: cancels the active review for the PR (auto or slash, queued or running). The acknowledgement worker replaces the review progress comment with a short failure-style notice:Cancelled by @login. Run \/review` to try again.` (no Head/Source/CI/Recon/specialist table). No review is active → short reply only./review force: cancels any queued or running review for the PR (same mechanism as/cancel), then queues a fresh review that resolves the latest head at execution time, all in one intake transaction. The acknowledgement worker completes the old run's check run and cancelled notice first, then posts the new run's queued progress stub and a short confirmation reply. With no active review it behaves like plain/review.- Review close cancel: when a PR is closed (merged or not), active review work is cancelled and the acknowledgement worker replaces the progress comment with
PR merged.orPR closed.(same short layout, no progress table). - Triage close cancel: when a PR is closed or merged, queued/running
/triagework is cancelled in the same intake transaction. Running triage stops at its next durable/Pi checkpoint. The acknowledgement worker posts a-1and a cancel notice. Separately, the writable checkout re-reads PR lifecycle state throughPrSurfaceimmediately before commit and push, and the publisher re-checks it after the push settles. Closed or merged state at either write-guard produces the terminalclosedno-push notice: a push that raced the close is not reported as a success and resolves nofixedthreads. Fork report-only, path, commit, push-or-nothing, never-force-push, and stale-head controls remain unchanged. - Review pointer link: on the second and later orchestrated runs for a PR, the Files-tab pointer links to the existing review summary comment when it can be verified. The first completed summary uses plain text only.
- CI summary: the progress stub and completed summary include a CI gate row for external checks on the PR head (excluding PR Agent’s own check). Ack uses a lightweight non-LLM snapshot. At publish, the worker waits/polls (
REVIEW_CI_SUMMARY_WAIT_*), selects one condensed, redacted, size-bounded CI context when CI is red (Actions job logs, or check output if logs are unavailable), and runs a small LLM turn to authorheadline/ failurereason+fixHint(server still owns status facts, check-run completeness, and HTML rendering). On a red head, annotation and job-log fetches run concurrently (REVIEW_CI_SUMMARY_FETCH_CONCURRENCY); workflow runs are filtered to the reviewed head before job listing, with an early exit atREVIEW_CI_SUMMARY_LOG_MAX_JOBS; raw logs are bounded to a failure-preserving intake window (REVIEW_CI_SUMMARY_LOG_RAW_TAIL_MULTIPLE× the per-job char budget: tail when it has an error signal, otherwise a window that still includes the last error line) before condensation. If CI is still pending at publish, the row stays pending; a laterworkflow_runorcheck_suitecompleted webhook enqueues a CI-refresh job that edits only the CI cell on the matching review summary comment for that head SHA (no full re-review). A refresh that arrives while a review is still queued or running is retained and retried on the same lane until it can patch that head or the attempt cap is exhausted. An incomplete check-run page (the retrieval cap) is unavailable, not passing or none, and is not a missing-Checks error. Known failures in that partial view stay failing; the headline notes the partial view. A later complete poll or refresh can replace the snapshot. Missing Checks: Read shows a grant-Checks row; missing Actions: Read on a failing head keeps the failure row and adds a grant-Actions note. The review still publishes either way. Caps:REVIEW_CI_SUMMARY_*inreviewConstants.ts. See ADR 0018. - Review map link: the summary links to the PR description's review map only when that section was published (read-first map mode). Short omit-mode descriptions have no map and no summary link.
- Worker concurrency: review, ask, acknowledgement, CI-refresh, description, triage, and verification jobs are capped per process by
REVIEW_CONCURRENCY(default2),ASK_CONCURRENCY(default1),ACK_CONCURRENCY(default2, also used foragent-work-ci-refresh),DESCRIPTION_CONCURRENCY(default1),TRIAGE_CONCURRENCY(default1), andVERIFICATION_CONCURRENCY(default1) via pg-boss workerlocalConcurrency(src/agentWork/worker.ts). Multi-replica deployments remain at-least-once at the worker layer. Effective cluster admission for a queue is approximatelyreplicas × localConcurrency(plusINSTALLATION_GROUP_CONCURRENCYfor group-scoped lanes). - GitHub rate-limit circuit (per run + shared MVP): after three consecutive primary/secondary rate-limit retries are exhausted inside a review/ask run, nonessential GitHub tools short-circuit for the rest of that run; a successful GitHub request resets the consecutive-failure counter. Emits
github_rate_limit_circuit_openedlogs andrate_limit_circuit_openedreview metrics. Opening a local circuit also upserts a Postgres shared circuit keyed byinstallation_id(github_installation_rate_limit_circuits:open_until,last_error_kind, default cooldownSHARED_RATE_LIMIT_CIRCUIT_COOLDOWN_MS= 60s). Other workers check that row at review/ask start and hydrate their local circuit open (github_shared_rate_limit_circuit_honored) so replicas do not immediately re-burst the same installation. Full Redis Bottleneck clustering remains optional (ADR 0004). - Tool surface: production review, ask, and verification agents investigate through one
execute({ code })tool. Scripts call the canonical workspace capabilities astools.listChangedFiles,tools.readWorkspaceFile,tools.searchWorkspace,tools.getWorkspaceDiff,tools.getWorkspaceBlame, andtools.resolveSymbol(ephemeral per-run symbol index). WhenCODE_INDEX_MODE=fts, reviews and ask also exposesearchCodeIndex(Postgres FTS navigation hints only — a followingtools.readWorkspaceFileremains mandatory before citing). Ask uses a ready snapshot for the PR head when one exists and otherwise registers the same tool with{ unavailable: true }. Verification uses the same prepared Local PR workspace through thosetools.*calls. Plus two Context7 tools. The review orchestrator hands off throughsubmit_specialist_brief, judges reports withpublish_thread, and finishes throughpublish_summary. Specialists submit onesubmit_findings_report. Description and triage still expose native workspace tools. GitHub reads and writes remain server-owned. See ADR 0011, ADR 0012, ADR 0033, and ADR 0002. - Library docs lookup: review and ask agents get Context7 tools (
resolveLibraryId,getLibraryDocs) that use the fixedhttps://context7.com/apiendpoint. The shared outbound policy validates library identifiers, trims and bounds query/topic text, redacts secret-shaped responses, and rejects URLs, multiline/prompt/comment/source/tool-output content before URL construction.CONTEXT7_API_KEYis optional and is sent only asAuthorization: Bearer ...; empty keys use anonymous fallback. See ADR 0002. - Pi-native agent runtime: Set
PI_PROVIDER/PI_MODELfor general sessions, optionalPI_ORCHESTRATOR_*andPI_FALLBACK_*, and an optionalmodels.jsoncatalog. Sharedsendfails when the SDK settles on an unrecovered provider error. A successful internal SDK retry still succeeds. Public cancellation stays fallback-ineligible even during retry backoff with no replacement assistant turn. Tool-round budget stops are not provider outages. Only availability-class exhaustion can start fallback from a committed Agent phase checkpoint. See ADR 0023. - Code Mode: Review, ask, and verification investigate the local PR workspace through one
execute({ code })tool. Scripts calltools.*workspace capabilities. Submit and publish tools stay native. AST fuel, a 25-call cap, a 15s wall clock, and bounded serialization keep a script from freezing the worker. Session abort (lease loss or cancel) aborts the script even if the JavaScript usestry/catch. See ADR 0033. - 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(code constant, default10000) is the webhook intake response budget. Intake that exceeds this budget minusGITHUB_WEBHOOK_RESPONSE_MARGIN_MSreturns503before GitHub reports a delivery timeout. Worker jobs are still supervised separately./triage: trigger-only autofix work type. Post/triageon the PR conversation to triage unresolved findings from current specialist runs and recognized legacy lens threads. Reply/triageinside a bot inline finding thread to scope the run to that finding./triage previewposts## PR Agent Triage Previewwith the would-be unified diff for the same scope and does not push or change threads./triage allthen replays those stored hunks (full PR even when posted on a thread; optionalexclude <thread ids>). Bulk without a completedtriage_previewfor the same PR head is a report-only refusal and does not check out. It does not start a second agent run. Bare/triagestill applies without a preview. Triage skips fork PR pushes, fixes same-repo findings in an isolated writable checkout, commits with validated messages, and pushes without force. Adismissedverdict requires an authorized non-bot maintainer decision fromMAINTAINER_DECISION_ASSOCIATIONSon the matching finding thread; ordinary, missing-metadata, and bot replies remain untrusted evidence. After a successful push, it replies onfixedthreads then resolves them. It resolvesalready-resolvedanddismissedthreads even when push is stale or there were no commits (skippedstays open). It upserts## PR Agent Triage. Fix commits use the human/triageissuer as git author and committer (profile or id-based noreply email) with a GitHub AppCo-authored-bytrailer; bot/app or unresolvable issuers keep App authorship without a redundant App co-author. Push still uses the installation token (App). The bot push triggers a normalsynchronizeverification run. Close/merge cancellation is terminal and no-push, including when the branch remains available. The writable workspacesearchWorkspacemay grep the checkout internally, but normalizes each result and applies the canonical sensitive/control-path policy to the path and its resolved symlink target before returning the path or matching text. Blocked matches are omitted and reported only with a non-sensitive filtered indicator; literal-query, timeout, result-cap, and output-budget behavior remains unchanged. GitHub App needs Contents: read/write for this command.- Verification runs: auto-triggered on
pull_requestsynchronizewhenFEATURE_VERIFICATION=auto(the default), or on demand with/verifywhen the feature ismanualorauto(offdisables both). Read-only: re-checks open bot inline findings against the current head. Automatic and slash-triggered runs share one Local PR workspace object for the session directory, search, and diffs.searchWorkspacesearches permitted tracked paths through that workspace's literal-search operation, then applies the same sensitive/control-path policy as triage before the result cap. Blocked paths are omitted and counted; the count is logged asverification_search_matches_filtered. Search output that exceedsLOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTESis treated as truncated, not a failed search.getWorkspaceDiffreturns the cached GitHub PR patch, including deleted paths and omitted-patch notices. Private Git metadata stays outside the agent-visible tree. Fixed and already-resolved threads are resolved without a new reply; if a prior verification stub exists, it is edited in place to a short fixed/already-resolved line so a stale still-open signal is not left behind. Still-open findings on files changed in the push update one verification stub comment in place. Dismissed findings require an authorized non-bot maintainer decision fromMAINTAINER_DECISION_ASSOCIATIONSon the matching finding thread; other reply text is untrusted evidence. Verification edits that stub (evidence + policy suggestion) and then resolves the thread. No ack reaction, progress comment, or summary comment is posted for a successful run. A terminal failure edits the existing CI cell for the bound execution head, or writes one bounded stub line, withRun \/verify` to try again.It edits only a bot-owned conversation comment. It does not open finding-thread replies or a new comment per finding. Silence on the PR means the run succeeded. If the live head moved after the run bound its SHA, publish is skipped, the skip log keeps both SHAs, and the work item completes degraded (publishDegraded) rather than as clean success. Slash/verify` has no automatic replacement. A later auto synchronize run is a separate item. - Policy suggestions for dismissed findings: when triage or verification dismisses a finding, the bot drafts a paste-ready
.pr-agent/*.mdcsuggestion. Verification grounds the suggestion in the checkout’s existing rules when exactly one rule matches the finding path (append fragment); otherwise it proposes a new.mdcstarter. Triage always proposes a new.mdcstarter. /askand@botmentions: interactive Q&A about PR code (and conversational follow-ups in the same thread). Shared intake admits work through durable actor, repository, and installation token buckets plus outstanding limits before insertingagent_work_items. When configured, the installation provider budget reserves a bounded token amount; each distinct ask computation's known Pi usage is counted once, and unknown usage consumes the reservation. The budget window rolls on wall-clock time while outstanding reservations stay reserved. Excess asks receive one static throttling reply and do not enteragent-work-ask. The ask queue remains separate from review and triage queues.@-mention of the app bot (same allowlist as slash commands) on the PR conversation or an inline review thread also enqueues an ask; the worker loads the containing thread transcript into the prompt. Explain-only — no severity/dismiss mutations. A terminal Ask failure reply is posted only when durable state has no confirmed delivered answer, and that reply is idempotent under theask:failure_replyoperation-intent key. See ADR 0005.- Lightweight review completion: automated reviews on docs-only trivial PRs may finish without an orchestrated review run under the trivial change exemption. See ADR 0010.
@octokit/plugin-throttlingpaces all installation-token REST calls (review tools, publish, reactions). File listing and patch caps are code constants insrc/settings/reviewConstants.ts:MAX_PR_FILES_LISTED(default300),MAX_PR_FILES_PATCH_BYTES(default500000).- Throttle hooks log
octokit_on_rate_limit/octokit_on_secondary_rate_limit. Circuit open logsgithub_rate_limit_circuit_opened. Capture a redacted sample when debugging production limits. - See ADR 0004 for policy (secondary-limit retries and truncation trade-offs).
-
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). The runtime stage installs Debiangitfromnode:22.22.0-bookworm-slim(2.39.x). Shared workspace search usesgit grep -nF -I -zand appliesmaxResultsplusLOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTESafter the process returns. It does not pass--max-count(Git 2.40+). -
Health (web):
GET /healthreturns200and plainok.GET /readyruns a PostgresSELECT 1and returns503when the database is unreachable (orchestrator readiness gating). -
Health (worker): the worker listens on
PORTfor the same paths.GET /healthis process liveness.GET /readyrequires registered queue consumers plus Postgres/pg-boss access (idle empty queues still ready). Compose wires the worker healthcheck to/ready. Continuous queue/DLQ diagnostics emit every 60s; see agent-work-ops.md. -
Webhook URL (default Compose ports):
http://<host>:7224/webhooks. -
DATABASE_URLin Compose:postgres://pr_agent:pr_agent@postgres:5432/pr_agent. -
Provider API keys (for example
OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_GENERATIVE_AI_API_KEY) are loaded bysrc/config.tsintomodelProviderKeys. Set them in.envbeside the GitHub fields or reviews fail at runtime in the worker. -
Custom Pi providers (
models.json): three ways to get a catalog into the container at/app/models.json(process cwd), or elsewhere viaMODELS_JSON_PATH:- Build-context copy (default image behavior): if repo-root
models.jsonis present when youdocker build, the runtime stage copies it to/app/models.json. If the file is absent, the build still succeeds (built-ins only). Dokploy Patches cancreateFile Pathmodels.jsonafter clone and before build — noMODELS_JSON_PATHand no Dockerfile edit required when using the image cwd. - Runtime bind mount: copy
models.json.exampleto a host file, then mount it into both web and worker. Create the host file before mounting — Docker turns a missing host path into a directory. MODELS_JSON_PATH: mount the catalog anywhere and point the env var at that path.
Example Compose fragment for a host mount:
services: pr-agent-web: volumes: - ./models.json:/app/models.json:ro pr-agent-worker: volumes: - ./models.json:/app/models.json:ro
- Build-context copy (default image behavior): if repo-root
-
Secrets: never commit
.env; keep Compose files off public pastebins.
cp .env.example .env
docker compose build
docker compose upCompose sets environment.PORT=7224 and 7224:7224 publishing. For a host port clash, change ports to for example 7227:7224 and keep container PORT at 7224.
Requires Docker Engine with Compose v2. env_file defaults to .env; use host env PR_AGENT_ENV_FILE for an alternate path.
The Compose postgres service sets shm_size: 128mb and conservative server tuning flags (shared_buffers, effective_cache_size, maintenance_work_mem, wal_compression, random_page_cost). Override them with the POSTGRES_SHARED_BUFFERS, POSTGRES_EFFECTIVE_CACHE_SIZE, POSTGRES_MAINTENANCE_WORK_MEM, POSTGRES_WAL_COMPRESSION, and POSTGRES_RANDOM_PAGE_COST Compose env vars. The base Compose file owns shm_size and those tuning flags; the production overlay only requires credentials via :? env vars.
PR_AGENT_ENV_FILE=/abs/path/to/.env docker compose up- Production boot uses a web/worker split (
ROLEenv). - Web:
processWebhookRequestEffectverifies signatures, parses payloads, and dispatches toWebhookHandlers, which callAgentWorkSchedulerfor Postgres intake and pg-boss enqueue. - Worker:
agentWorkWorkerLiveconsumes acknowledgement, review, ask, description, triage, verification, and CI-refresh queues; PR-surface I/O and LLM runs happen viaexecutors/. - PR actor lease cutover (one-time): the release that introduces
pr_actor_leases(ADR 0030) changes the review, description, triage, and verification queues fromkey_strict_fifotostandard. Drain old workers before starting new ones: stoppr-agent-worker, let in-flight jobs finish or expire, then deploy. Migration 023 flips the policy on existing queue rows (pg-boss never changes a policy itself), and boot logsagent_queue_policy_mismatchif any leased queue was not flipped. Mixed old/new workers are still unsafe — old workers fence on per-job queue policy while new workers fence on the lease — so the drain is required even though the flip is automatic. There is nothing to backfill.
nub src/index.tsloads.envautomatically for local development.nub watch src/index.tsrestarts on source, tsconfig, and env changes.GITHUB_APP_PRIVATE_KEYmust be a valid PEM key. For local-only dev:openssl genrsa 2048 > key.pemand set the escaped PEM in.env.- Tunnel webhooks (e.g. smee.io) to local
PORT, then point the GitHub App webhook at the smee URL forwarding to/webhooks. - If switching from a prior pnpm- or npm-installed tree, delete
node_modulesbefore the firstnub install. - Vercel site deploys install pinned Nub in
site/vercel.json(npm install -g @nubjs/nub@…+nub ci), then build withnub --node run build(same plain-Node path assite:build). The site serves a dense agent profile at/llms.txtplus queryableGET /llms?query=andGET /llms/json?query=fromsite/lib/llmsKnowledge.ts. - Docker image installs with Nub only (
nub cifor build deps;nub prune --prodfor the runtime tree). Nocorepack/pnpm deployin the Dockerfile..dockerignorekeepssite/package.jsonin the build context so the workspace lockfile stays valid under frozen installs; the rest ofsite/stays excluded.
Canonical quick start steps live in README.md Host with Docker Compose.
| Script | Purpose |
|---|---|
nub src/index.ts / nub run dev |
Run src/index.ts (ROLE env) |
nub watch src/index.ts |
Auto-restart dev entry |
nub run build |
Compile to dist/ |
nub run start / node dist/index.js |
Run compiled dist/ |
nub run typecheck |
tsc --noEmit (src/ only) |
nub run lint |
Type-aware Oxlint (includes site/) |
nub run lint:backend |
Type-aware Oxlint excluding site/ |
nub run lint:fix |
Oxlint with safe fixes |
nub run fmt |
Format with Oxfmt |
nub run fmt:check |
Check formatting |
nub run check:code |
typecheck + lint + fmt:check |
nub run check:effect-versions |
Verify pinned Effect deps |
nub run check:prod-deps |
Production dependency graph guard |
nub run test |
Vitest (test/**/*.test.ts) |
nub run test:watch |
Vitest watch mode |
nub run test:integration |
Vitest integration suite (requires Postgres; fails fast if unreachable) |
nub run test:integration:inventory |
Same suite, but may skip DB cases when DATABASE_URL is unset |
nub run --node test |
Vitest via plain Node (escape hatch) |
nub run site:dev |
Landing site local dev (pr-agent-landing) |
nub run site:build |
Landing site production build |
nub run site:generate-og |
Generate landing OG assets |
Type awareness comes from .oxlintrc.json options.typeAware (lint scripts do not pass --type-aware). Keep nub run typecheck as separate tsc. Type-aware lint requires oxlint-tsgolint (dev dependency). Registry cooling-window settings live only in nub.jsonc (install.minimumReleaseAge, install.minimumReleaseAgeExclude); edit that file when adding or removing temporary excludes.
nub run check:effect-versions enforces pinned versions:
effect@3.22.0@effect/platform@0.97.0@effect/platform-node@0.108.0
nub run test runs this gate before Vitest (pretest).
Agent index: AGENTS.md.
-
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 0007). -
Triage workspace boundary: direct reads, search results, edits, staging, and commits use the canonical triage sensitive/control-path policy. Search may inspect the checkout internally, but it never returns a blocked path or its matching text; symlink targets are resolved before the decision.
-
Verification workspace search:
searchWorkspaceuses the Local PR workspace literal-search operation and that same sensitive/control-path predicate on permitted tracked paths. Blocked hits are withheld before the result cap and counted; the count is logged asverification_search_matches_filtered. Search output overLOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTESreturns as truncated rather than failing. Read tools keep their existing path gates. Diff tools read cached PR patches and keep the sensitive-path gate without requiring a deleted file to exist at head. -
Triage report comments run through the same public-output redactor before upsert to GitHub (normal and report-only paths).
-
Telemetry redaction uses one canonical recursive sanitizer at logging and analytics boundaries (
src/security/sanitizeTelemetryValue.ts). It handlesAppErrormessages, contexts, raw values, causes, arrays, objects, Error fields, and circular references across camel- and snake-case payloads. Secret-shaped strings and sensitive keys are replaced while safe codes, classifications, work IDs, provider kinds, and resource identifiers remain available. PostHog is optional: enabled only whenPOSTHOG_PROJECT_TOKENis non-empty and sink construction succeeds (src/analyticsfacade; empty token means no SDK load). Failed construction or reinitialization restores a no-op sink with analytics disabled; the PostHogbefore_sendsanitizer is the final recursive backstop for explicit and autocaptured exceptions (ADR 0007). -
Classified external failures — Terminal and soft-fail agent-work events carry
failure_domain(provider|github|internal|unknown),error_kind(provider:auth/quota/billing/rate_limit/timeout; GitHub:auth/forbidden/not_found/validation/rate_limit; plusvalidation/publish/cancelled/superseded/unknown), and sanitizederror_message. Query these onwork item failed,ask failed,description failed,triage failed, andverification failed. Durable"work item failed"also keepsprovider_error_kindwhenfailure_domain === "provider". Superseded/cancelled/stale-head runs usefailure_domain=internalwitherror_kindin{superseded,cancelled}— never provider. Matching evlog fields use camelCase (failureDomain,errorKind,errorMessage) onreview_not_published,agent_publish_fallback,agent_work_failed, and siblings. Classification is logs/analytics only. PR-facing failure notices stay neutral. -
Review profile. Each executed review sends one
"review profiled"event. A review that throws after claim also sends this event. The analytics facade sends the event. The distinct ID is the installation.The
outcomeproperty ispublished,failed,degraded,superseded, orlightweight. The event includeswork_item_idandattempt_count. The event includesqueue_ms,review_ms, andtotal_ms.queue_msis claim start minus create time.review_msis finish minus claim start.total_msis finish minus create time.The event also includes provider, model, phase, token, cache, tool, finding, and publish fields. These fields come from
ReviewRunMetricsSnapshot. A failed outcome addsfailure_domainanderror_kind. A provider failure also addsprovider_error_kind. The event addsphaseonly when the phase is a known safe value. The event omitserror_messageand the thrown message. The event omits prompts, source, model text, tool payloads, paths, hashes, and URLs.The event includes
wall_clock_ms,provider_output_tokens, andgeneration_ms. Whengeneration_msis greater than 0, the event includesprovider_output_tps.provider_output_tpsequals tokens divided by generation seconds. The event also includestoken_coverage. Use these fields to separate slow generation from failures. Degraded runs showoutcome=degraded, extra publish attempts, or tool errors. Do not treat wall-clock alone as provider TPS. A suggested dashboard gate iswall_clock_ms >= 180000. Treat the run as provider-TPS-slow when TPS is under 10 without a retry signal.A later check-run or cleanup failure does not send a second event. Durable
"work item failed"andcaptureExceptionstay on the durable-work path. An emptyPOSTHOG_PROJECT_TOKENkeeps capture as a no-op. -
Prompt cache excellence —
review_run_completedincludes rawcacheReadTokens/cacheWriteTokens/ optionalcacheWrite1hTokens, plus derivedcacheHitRate=cacheRead / (providerInput + cacheRead + cacheWrite)andcacheWriteAmplification=cacheWrite / max(cacheRead, 1). All four ratio fields arenullwhen provider cache usage was never known for the run. High hit rate with low write amplification means the stable system+tools prefix is paying off; high write amplification or null cache fields after a long run usually means provider usage metadata is missing or the prefix is still busting. Policy: ADR 0025. -
Structured logging uses evlog with
service: pr-agent.LOG_LEVELmaps to evlogminLevel(defaultinfo).LOG_MAX_WIDE_EVENTS(code constant, default128) caps sub-events per webhook/worker operation.LOG_REDACT(default true) redacts secret-shaped substrings from logs.LOG_PRETTYdefaults to off in production (JSON lines). -
Production logging should stay at
infounless debugging a specific review run (LOG_LEVEL=debug).