Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,12 @@ REVIEW_ANCHOR_MENU_MAX_RANGES_PER_FILE=20
# --- Local PR workspace ---
LOCAL_WORKSPACE_CLONE_TIMEOUT_MS=60000
LOCAL_WORKSPACE_FETCH_TIMEOUT_MS=60000
LOCAL_WORKSPACE_MAX_MATERIALIZED_FILES=500
LOCAL_WORKSPACE_SEARCH_MAX_FILES=500
LOCAL_WORKSPACE_MAX_FILE_BYTES=1000000
LOCAL_WORKSPACE_MAX_TOTAL_BYTES=50000000
LOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTES=50000000
LOCAL_WORKSPACE_MAX_DIFF_BYTES=5000000
LOCAL_WORKSPACE_MIN_FREE_SPACE_BYTES=500000000
LOCAL_WORKSPACE_STALE_CLEANUP_AGE_SECONDS=86400
LOCAL_WORKSPACE_MAX_BLAME_DEEPEN_COMMITS=1000

# --- Ask agent ---
MAX_ASK_TOOL_ROUNDS=12
Expand Down
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This file is **domain language only** — not a specification of how the system
- **Description run** — Execution of an agent work item that generates a concise PR description (type, summary bullets, optional Mermaid diagram, file walkthrough) and publishes by merging into the pull request body. Triggered automatically on `pull_request` open/sync and by `/describe`.
- **Description queue** — pg-boss `agent-work-description` lane with worker concurrency `DESCRIPTION_CONCURRENCY` (default `1`) and per-PR `key_strict_fifo` singleton keys.
- **Description agent block** — Generated markdown under `## PR Agent Description` in the PR body. Re-runs replace only this block; content above the header is treated as user-authored.
- **Local PR workspace** — A temporary local representation of a pull request used by a **Review run**, **Ask run**, or **Description run** so the agent can inspect pull request code without using GitHub read tools as its investigation surface.
- **Local PR workspace** — A temporary shallow checkout of the pull request head plus server-owned PR diff metadata, used by a **Review run**, **Ask run**, or **Description run** so the agent can inspect the full repository tree at the PR head without using GitHub read tools as its investigation surface.
- **PR repository view cache** — Worker-process ref-counted cache keyed by `owner/repo#pr:headSha` so concurrent jobs for the same PR commit share one git clone; released when the last job finishes.
- **Review run** — Execution of an agent work item that performs an LLM + tool pass scoped to a pull request (automated `pull_request` events, `/review`, `/review-security`, or `/review-quality`). Each run is a **single-pass review**: one investigation sweep ending in one `submitReview` with all evidenced P0–P2 findings, not an open-ended multi-publish review loop.
- **Review phase** — Named stage within a **Review run** harness loop, used for metrics and retry attribution: **investigation** (GitHub tool rounds before submit), **pre_submit** (nudge to call submitReview after investigation), **validation_repair** (fix ReviewPayload after schema/anchor errors), **publish_recovery** (retry after publish did not succeed), **plaintext_fallback** (maintainer failure notice when structured publish is exhausted).
Expand Down
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

> **Async by design.** Webhooks return **`200`** after durable intake (Postgres + pg-boss enqueue). Reactions, progress comments, reviews, descriptions, and ask answers publish on **`ROLE=worker`** and may appear seconds later.

PR Agent is a GitHub App that enqueues durable **agent work items** (reviews, descriptions, asks) from webhooks and slash commands, then runs LLM agent loops on workers using Octokit tools and optional local PR workspaces.
PR Agent is a GitHub App that enqueues durable **agent work items** (reviews, descriptions, asks) from webhooks and slash commands, then runs LLM agent loops on workers using local PR workspaces (full shallow checkout of the PR head for context) and GitHub APIs for publish.

Domain terms: [CONTEXT.md](CONTEXT.md). Configuration: [docs/configuration.md](docs/configuration.md). Behaviour and deployment: [docs/operations.md](docs/operations.md). Queue runbook: [docs/agent-work-ops.md](docs/agent-work-ops.md).

Expand Down Expand Up @@ -91,10 +91,10 @@ Developer scripts: see [docs/operations.md](docs/operations.md#development).

LLM runs happen on **`ROLE=worker`** only. Pick a **runner** with `AGENT_PROVIDER`, then set model and credentials.

| Runner | `AGENT_PROVIDER` | Model env | Credentials |
|--------|------------------|-----------|-------------|
| Pi (default) | `pi` | `PI_PROVIDER`, `PI_MODEL` | Provider API key env vars (see below) |
| Cursor SDK | `cursor` | `PI_MODEL` (Cursor model id) | `CURSOR_API_KEY` (required) |
| Runner | `AGENT_PROVIDER` | Model env | Credentials |
| ------------ | ---------------- | ---------------------------- | ------------------------------------- |
| Pi (default) | `pi` | `PI_PROVIDER`, `PI_MODEL` | Provider API key env vars (see below) |
| Cursor SDK | `cursor` | `PI_MODEL` (Cursor model id) | `CURSOR_API_KEY` (required) |

Full tunables: [docs/configuration.md](docs/configuration.md). Cursor integration: [ADR 0013](docs/adr/0013-cursor-sdk-provider.md).

Expand Down Expand Up @@ -155,21 +155,21 @@ General bug-and-correctness reviews run on PR open and sync. **`/review-security

## Features

| Capability | Auto on PR | Slash command | Notes |
|------------|------------|---------------|-------|
| General review | opened / sync / reopen | `/review` | `## PR Agent Review`; inline P0 to P2 on Files tab when present |
| PR description | same | `/describe` | Merges under `## PR Agent Description` |
| Security lens | No | `/review-security` | `## PR Agent Security Review` |
| Quality lens | No | `/review-quality` | `## PR Agent Quality Review` |
| Ask | No | `/ask <question>` | PR conversation or inline diff **code anchor** |
| Help | No | `/help` | Worker-published guidance |
| Lightweight auto-review | docs-only trivial PRs | No | Skips full **review run**; see [ADR 0014](docs/adr/0014-lightweight-review-completion.md) |

| Deployment | Supported |
|------------|-----------|
| Docker Compose (web + worker + Postgres) | Yes |
| Bare Node + Postgres | Yes |
| Cursor provider (`AGENT_PROVIDER=cursor`) | Yes |
| Capability | Auto on PR | Slash command | Notes |
| ----------------------- | ---------------------- | ------------------ | ----------------------------------------------------------------------------------------- |
| General review | opened / sync / reopen | `/review` | `## PR Agent Review`; inline P0 to P2 on Files tab when present |
| PR description | same | `/describe` | Merges under `## PR Agent Description` |
| Security lens | No | `/review-security` | `## PR Agent Security Review` |
| Quality lens | No | `/review-quality` | `## PR Agent Quality Review` |
| Ask | No | `/ask <question>` | PR conversation or inline diff **code anchor** |
| Help | No | `/help` | Worker-published guidance |
| Lightweight auto-review | docs-only trivial PRs | No | Skips full **review run**; see [ADR 0014](docs/adr/0014-lightweight-review-completion.md) |

| Deployment | Supported |
| ----------------------------------------- | --------- |
| Docker Compose (web + worker + Postgres) | Yes |
| Bare Node + Postgres | Yes |
| Cursor provider (`AGENT_PROVIDER=cursor`) | Yes |

Slash commands are **case-sensitive** and must start the first non-empty line of a new comment. Full behaviour: [docs/operations.md](docs/operations.md).

Expand Down Expand Up @@ -223,7 +223,7 @@ flowchart LR
1. **Web** ([`processWebhookRequestEffect`](src/effect/programs/processWebhookRequestEffect.ts)): verify signature, parse payload, durable dedupe, schedule **agent work items**.
2. **Scheduler** ([`AgentWorkScheduler`](src/agentWork/scheduler.ts)): write Postgres rows and enqueue pg-boss jobs (ack, review, ask, description).
3. **Ack worker**: acknowledgement reaction and **review progress comment** stub before long runs.
4. **Review / ask / description workers** ([`executors/`](src/agentWork/executors/)): installation token, optional **local PR workspace**, agent harness, **PR-surface I/O**.
4. **Review / ask / description workers** ([`executors/`](src/agentWork/executors/)): installation token, **local PR workspace** (depth-1 full head checkout + GitHub PR-file diff metadata), agent harness, **PR-surface I/O**.
5. **Reviews** ([`runFullPrReview`](src/review/reviewRun.ts)): investigation tools, then one structured **`submitReview`** publish path.

Queue inspection and recovery: [docs/agent-work-ops.md](docs/agent-work-ops.md). Architecture ADR: [docs/adr/0009-durable-agent-work.md](docs/adr/0009-durable-agent-work.md).
Expand Down
3 changes: 2 additions & 1 deletion docs/adr/0015-agent-runner-local-pr-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
## Status

Accepted. Supersedes the GitHub-API-only repository signal and `PI_PROVIDER=cursor`
selection parts of [ADR 0013](0013-cursor-sdk-provider.md).
selection parts of [ADR 0013](0013-cursor-sdk-provider.md). Changed-file materialization
and local-git diff authority are superseded by [ADR 0017](0017-full-context-local-pr-workspace.md).

## Context

Expand Down
33 changes: 33 additions & 0 deletions docs/adr/0017-full-context-local-pr-workspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ADR 0017: Full-context local PR workspace

## Status

Accepted. Supersedes the changed-file materialization and local-git diff authority parts of [ADR 0015](0015-agent-runner-local-pr-workspace.md).

## Context

ADR 0015 introduced a **Local PR workspace** with a private git object database and an agent-visible tree that initially contained only changed files materialized from `git show`. Agents could lazy-read other paths, but investigation still leaned on a partial tree and local `git diff` for changed paths and inline anchor hints.

That layout made it easy to miss surrounding callers, shared types, and config when the PR diff alone was insufficient. A depth-1 fetch of the full PR head is fast enough to expose the whole git-tracked tree while keeping `.git`, credentials, and hooks outside the sandbox.

## Decision

1. **Full head checkout** — After durable head-SHA resolution, prepare a shallow (`--depth=1`), no-tags, no-submodules checkout of `refs/pull/<n>/head` into the agent-visible tree via a private git directory (`GIT_WORK_TREE`); no partial clone / lazy blobs.
2. **GitHub PR files for diff metadata** — Changed paths, unified diff patches (subject to existing caps), and commentable RIGHT-side anchor ranges come from `pulls.listFiles` via shared [`src/github/listPullRequestFiles.ts`](../src/github/listPullRequestFiles.ts), not local `git diff base...head`.
3. **PR-scoped findings** — The full tree is context for investigation; review findings remain tied to issues introduced or exposed by the PR. Publish-time inline anchors still use the server diff index.
4. **Search budgets** — Replace materialization caps with `LOCAL_WORKSPACE_SEARCH_MAX_FILES` and `LOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTES` for `searchWorkspace`; single-file reads remain capped by `LOCAL_WORKSPACE_MAX_FILE_BYTES`.
5. **Security** — Committed sensitive-looking files remain in the checkout (no redaction). `.git`, token files, askpass, hooks, and symlinks stay out of or are removed from the agent tree. Ask sensitive-path gates still apply to local tool reads.

## Consequences

- Worker disk and network per run increase versus changed-file-only materialization; mitigated by depth-1 fetch and existing free-space checks.
- One additional GitHub API call per workspace prepare (`listFiles`); investigation no longer depends on merge-base history in the private git dir.
- `LOCAL_WORKSPACE_MAX_MATERIALIZED_FILES` and `LOCAL_WORKSPACE_MAX_TOTAL_BYTES` are removed; deployments must adopt the new search env names.
- Tests must assert full-tree presence and PR-metadata-driven diff index separately from git checkout.

## Alternatives considered

- **Keep changed-file-only materialization** — Rejected; does not meet the full-context goal.
- **Local git diff as authority** — Rejected for anchors; depth-1 head fetch does not guarantee merge-base reachability without extra history.
- **Partial clone (`--filter=blob:none`)** — Rejected; lazy blobs weaken immediate full-context reads.
- **Omit sensitive paths from checkout** — Rejected; would hide config the agent may need; documented trade-off instead.
5 changes: 2 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ Import convention: `import { … } from "../settings/index.js"` for constants; `
| Anchor menu max ranges | `REVIEW_ANCHOR_MENU_MAX_RANGES_PER_FILE` | `20` | cap ranges per file in anchor menu |
| Workspace clone timeout | `LOCAL_WORKSPACE_CLONE_TIMEOUT_MS` | `60000` | git clone/setup budget |
| Workspace fetch timeout | `LOCAL_WORKSPACE_FETCH_TIMEOUT_MS` | `60000` | git fetch/diff budget |
| Workspace file cap | `LOCAL_WORKSPACE_MAX_MATERIALIZED_FILES` | `500` | max files exposed to agent-visible tree |
| Workspace search file cap | `LOCAL_WORKSPACE_SEARCH_MAX_FILES` | `500` | max files scanned per `searchWorkspace` call |
| Workspace single-file cap | `LOCAL_WORKSPACE_MAX_FILE_BYTES` | `1000000` | max file bytes readable by local tools |
| Workspace total cap | `LOCAL_WORKSPACE_MAX_TOTAL_BYTES` | `50000000` | max materialized bytes |
| Workspace search byte cap | `LOCAL_WORKSPACE_SEARCH_MAX_TOTAL_BYTES` | `50000000` | max bytes scanned per `searchWorkspace` call |
| Workspace diff cap | `LOCAL_WORKSPACE_MAX_DIFF_BYTES` | `5000000` | max local diff bytes returned to tools |
| Workspace free space min | `LOCAL_WORKSPACE_MIN_FREE_SPACE_BYTES` | `500000000` | fail setup below this free-space threshold |
| Workspace stale cleanup | `LOCAL_WORKSPACE_STALE_CLEANUP_AGE_SECONDS` | `86400` | startup cleanup age for leaked temp dirs |
| Workspace blame deepen | `LOCAL_WORKSPACE_MAX_BLAME_DEEPEN_COMMITS` | `1000` | best-effort blame history deepen cap |
| Log level | `LOG_LEVEL` | `info` | |
| Max wide sub-events | `LOG_MAX_WIDE_EVENTS` | `128` | |
| Pretty logs | `LOG_PRETTY` | dev `true`, prod `false` | |
Expand Down
2 changes: 1 addition & 1 deletion src/agent/askPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function buildAskSystemPrompt(): string {
"Your job is to answer one specific question about the PR code clearly and accurately.",
"",
"## How to investigate",
"- Use local workspace tools to read the PR diff, file contents, and related code.",
"- Use local workspace tools to read the PR diff, the full checkout, and related code.",
"- When a code anchor (file, lines, diff hunk) is provided, start there, then follow symbols to definitions and usages.",
"- For third-party library behavior, use resolveLibraryId then getLibraryDocs before claiming how an API works.",
"- Prefer `listChangedFiles` and `getWorkspaceDiff` before reading whole files when possible.",
Expand Down
7 changes: 5 additions & 2 deletions src/agent/askRunSetup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { logDebug } from "../evlog.js";
import { sanitizeLogMessage } from "../security/sanitizeLogMessage.js";
import { buildAskGithubTools, createAskPathGate } from "./askSafety.js";
import { buildLocalWorkspaceTools } from "./localWorkspaceTools.js";
import { buildLocalWorkspaceTools, workspaceToolLimitsFromConfig } from "./localWorkspaceTools.js";
import { createRefreshableToolExecutors } from "./providers/cursor/refreshableGithubTools.js";
import type { AskRunParams } from "./askRun.js";

Expand All @@ -15,7 +15,10 @@ export function buildAskRunSetup(params: AskRunParams) {

const refreshableGh = params.workspace
? {
bundle: buildLocalWorkspaceTools(params.workspace, { pathGate, extraAllowedPaths }),
bundle: buildLocalWorkspaceTools(params.workspace, workspaceToolLimitsFromConfig(cfg), {
pathGate,
extraAllowedPaths,
}),
refreshBeforeTool: async () => undefined,
}
: createRefreshableToolExecutors({
Expand Down
9 changes: 7 additions & 2 deletions src/agent/askSafety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ export function createAskPathGate(): AskPathGate {
};
}

export function pathAllowedForAsk(path: string, gate: AskPathGate): boolean {
const normalized = path.replace(/\\/g, "/");
if (!isSensitivePath(normalized)) return true;
return gate.prChangedPaths.has(normalized);
}

export function assertPathAllowedForAsk(path: string, gate: AskPathGate): void {
const normalized = path.replace(/\\/g, "/");
if (!isSensitivePath(normalized)) return;
if (gate.prChangedPaths.has(normalized)) return;
if (pathAllowedForAsk(normalized, gate)) return;
throw new Error(
`getFileContent blocked for sensitive path "${normalized}" (not in this PR's changed files). Ask about files touched by the PR instead.`,
);
Expand Down
6 changes: 4 additions & 2 deletions src/agent/descriptionRunSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Config } from "../config.js";
import type { LocalPrWorkspace } from "../prWorkspace/localPrWorkspace.js";
import { createAskPathGate } from "./askSafety.js";
import { buildGithubTools } from "./githubTools.js";
import { buildLocalWorkspaceTools } from "./localWorkspaceTools.js";
import { buildLocalWorkspaceTools, workspaceToolLimitsFromConfig } from "./localWorkspaceTools.js";
import { createRefreshableToolExecutors } from "./providers/cursor/refreshableGithubTools.js";
import { descriptionSystemPrompt } from "./descriptionSystemPrompt.js";
import { buildDescriptionUserContent } from "./descriptionUserMessage.js";
Expand Down Expand Up @@ -67,7 +67,9 @@ export function buildDescriptionRunSetup(params: {
githubToolNames: new Set([TOKEN_REFRESH_TOOL]),
build: (activeToken) => {
if (workspace) {
return buildLocalWorkspaceTools(workspace, { pathGate });
return buildLocalWorkspaceTools(workspace, workspaceToolLimitsFromConfig(cfg), {
pathGate,
});
}
return buildGithubTools(activeToken, {
maxPrFilesListed: cfg.maxPrFilesListed,
Expand Down
2 changes: 1 addition & 1 deletion src/agent/descriptionSystemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
export const descriptionSystemPrompt = [
"You are a senior engineer writing a concise pull request description for reviewers.",
"",
"Use the local workspace tools to inspect the PR: list changed files, read the workspace diff, and open files when needed.",
"Use the local workspace tools to inspect the PR: list changed files, read the workspace diff, and open any file in the full checkout when needed.",
"Focus on what changed and why it matters. Prefer facts from the diff over the existing PR title or body.",
"",
"When you have enough context, call submitDescription exactly once with a DescriptionPayload object.",
Expand Down
Loading
Loading