PatchPilot is a self-hosted GitHub issue-to-PR agent. Comment /agent fix ... on an issue, and PatchPilot clones the repository, plans and applies a patch with an LLM, runs the repository's own test command, and opens a pull request for a human to review.
It is a backend system, not a chatbot: every step is a durable task record with a timeline, tool-call and model-call audit, captured test output, and safety gates that decide whether the run may continue. It never pushes to a default branch and never merges.
- Stack: Java 21, Spring Boot 3.5, MySQL 8 + Flyway, MyBatis-Plus, an OpenAI-compatible model client, React 19 + Vite + TypeScript dashboard, Docker Compose.
- Tests: about 700 JUnit tests for the backend and 120 Vitest tests for the dashboard, both run by GitHub Actions.
sequenceDiagram
participant GH as GitHub
participant WH as Webhook + safety gates
participant Q as MySQL task queue
participant W as Worker
participant LLM as OpenAI-compatible model
GH->>WH: issue_comment "/agent fix ..."
WH->>WH: verify HMAC signature, allowlists, rate limit, trigger policy
WH->>Q: create durable task
WH-->>GH: status comment: accepted
W->>Q: claim task (visibility timeout, retries)
W->>W: clone repo, create branch, detect build system
W->>LLM: fix plan as JSON (summary, target files, steps, risk)
W->>LLM: full-file edits for the planned files
W->>LLM: independent patch review (approve / reject)
W->>W: diff risk gate, then allowlisted verification (e.g. mvn test)
W->>GH: push branch, open pull request, update status comment
- Trigger. A GitHub
issue_commentwebhook arrives. The signature is verified, then the comment passes trigger-user and repository allowlists, per-user/repository/issue rate limits, an automatic quarantine for repeat offenders, and a deterministic trigger policy that rejects vague ("fix it") or dangerous ("delete the production database") instructions. Rejections are recorded and answered on the issue with a safe refusal comment. - Durable task. Accepted triggers become a
fix_taskrow plus a queue item in MySQL. A poller claims work with a visibility timeout, records worker heartbeats, retries transient failures with backoff, and recovers tasks that a crashed worker left behind. Every state change is a timeline event. - Workspace. The worker clones the repository into an isolated task directory, creates a
patchpilot/<task-id>branch, and detects the build system with a language adapter (15 adapters, table below). Unsupported repositories fail fast before any model call. - Patch. The plan-driven workflow makes three model calls: a JSON fix plan, full-file edits limited to the planned files (at most three), and an independent patch review that can reject the edits. A workspace path guard blocks protected files and symlink escapes; a diff risk gate blocks secret-like added lines, binary patches, protected paths, and overly broad patches and parks the task as
PENDING_REVIEWfor an explicit operator approval. - Verify. The adapter's verification command (
mvn test,npm test,cargo test, ...) runs from a fixed allowlist. Model output can never choose the command. Output, exit code, and duration are stored with the task. - Publish. On green tests the worker commits as the PatchPilot author, pushes the branch, opens a pull request whose body lists the adapter, verification results, and review evidence, and updates the issue status comment with the PR link. Failures are reported on the issue with a failure category and next action.
The demo target is bingqin2/patchpilot-demo-java, a tiny Maven project whose Calculator.divide throws a bare ArithmeticException on a zero divisor while a test expects IllegalArgumentException("divisor must not be zero"). Issue #1 describes the expected behaviour; PatchPilot answered it with pull request #2.
The run was executed by the Docker Compose stack on a laptop with Tencent Hy3 as the model provider (reasoning_effort=high):
| Time | Step | Evidence |
|---|---|---|
| 0 s | Trigger accepted, task created and claimed by the worker | timeline events TRIGGER_ACCEPTED, TASK_CREATED, RUNNING |
| 1 s | Clone, branch patchpilot/<task-id>, adapter java/maven detected, issue context loaded |
tool calls WorkspaceService, LanguageAdapterRegistry, IssueContextService |
| 1 s to 32 s | Fix plan, full-file edit of Calculator.java, independent review (APPROVE, confidence HIGH) |
three recorded model calls with prompts, replies, and token counts |
| 33 s | Diff risk gate passed (three added lines, no secrets or protected paths) | tool calls DiffTool, GeneratedDiffRiskGate |
| 33 s to 44 s | mvn test in the worker container: 5 tests, 0 failures, exit code 0 |
captured test run with full output |
| 47 s | Commit, push, pull request opened with the verification and review evidence in its body | PR_CREATED, COMPLETED |
The resulting diff is the three-line guard the issue asked for and nothing else. The first attempt at this run failed at the review gate because Hy3 returned its confidence as a number rather than a string; that failure, the parser fix, and the retry are all recorded in the same task history, which is the point of keeping every step durable.
Prerequisites: Docker Desktop, a GitHub repository to test on, a fine-grained GitHub token for that repository with Contents: read and write, Issues: read and write, Pull requests: read and write, and Metadata: read, an API key for any OpenAI-compatible chat-completions endpoint, and a tunnel tool such as cloudflared or ngrok so GitHub can reach your machine.
git clone https://github.com/bingqin2/PatchPilot.git
cd PatchPilot
cp .env.example .env
# edit .env: PATCHPILOT_GITHUB_TOKEN, PATCHPILOT_GITHUB_WEBHOOK_SECRET,
# PATCHPILOT_ADMIN_TOKEN, PATCHPILOT_AGENT_API_KEY (and model/base URL)
docker compose --env-file .env up --buildThis starts MySQL, the backend on http://127.0.0.1:8080, and the dashboard on http://127.0.0.1:5173. Check the backend:
curl http://127.0.0.1:8080/health
curl -H "X-PatchPilot-Admin-Token: $PATCHPILOT_ADMIN_TOKEN" http://127.0.0.1:8080/api/configuration/summaryExpose the webhook endpoint and register it on GitHub:
cloudflared tunnel --url http://127.0.0.1:8080In the test repository open Settings → Webhooks → Add webhook: payload URL https://<your-tunnel-host>/api/github/webhook, content type application/json, secret equal to PATCHPILOT_GITHUB_WEBHOOK_SECRET, and only the Issue comments event.
Now open an issue and comment:
/agent fix Add null handling to Calculator.divide and cover it with a test
PatchPilot answers on the issue within a few seconds, and the dashboard shows the task moving through RUNNING, RUNNING_TESTS, PR_CREATED, and COMPLETED. For a deterministic smoke test that does not depend on model quality, use the built-in replacement instruction on an existing file:
/agent fix replace docs/demo.md PatchPilot smoke test
The docs/demo-repositories/ folder contains a minimal fixture for every supported build system; push one of them to a test repository to try the full loop on a small project.
| Build system | Detected by | Verification command |
|---|---|---|
| Java / Maven | pom.xml |
mvn test (or ./mvnw test) |
| Java / Gradle | build.gradle, build.gradle.kts, gradlew |
gradle test (or ./gradlew test) |
| Go | go.mod |
go test ./... |
| Rust / Cargo | Cargo.toml |
cargo test |
| Node / npm, pnpm, yarn, Bun | package.json with scripts.test plus the lockfile |
npm test, pnpm test, yarn test, bun test |
| Python / pytest | pytest config or dependency | python3 -m pytest |
| Python / Poetry, uv | pyproject.toml tool section plus pytest |
poetry run pytest, uv run pytest |
| Python / tox, nox, hatch | tox.ini, noxfile.py, hatch config |
tox, nox, hatch test |
| Custom profile | .patchpilot/verification-profile.json |
any command from the allowlist above |
Adapters are explicit classes under language/impl; adding one means implementing detection and returning an allowlisted command. The backend Docker image ships all of these toolchains so verification can run inside the container.
Copy .env.example to .env. The important variables:
| Variable | Purpose |
|---|---|
PATCHPILOT_GITHUB_TOKEN |
Fine-grained token used to clone, push, comment, and open pull requests. |
PATCHPILOT_GITHUB_WEBHOOK_SECRET |
HMAC secret shared with the GitHub webhook. |
PATCHPILOT_GITHUB_WEBHOOK_PUBLIC_BASE_URL |
Optional public base URL of the backend, shown in the dashboard. |
PATCHPILOT_ADMIN_TOKEN |
Required header X-PatchPilot-Admin-Token for every /api/* call except the webhook. |
PATCHPILOT_AGENT_BASE_URL, PATCHPILOT_AGENT_MODEL, PATCHPILOT_AGENT_API_KEY |
OpenAI-compatible provider (defaults to https://api.openai.com/v1 and gpt-4.1-mini). |
PATCHPILOT_AGENT_REASONING_EFFORT |
Optional reasoning_effort value sent with every model call, for providers that support it (for example high on Tencent Hy3). Leave empty to omit the field. |
PATCHPILOT_ALLOWED_TRIGGER_USERS, PATCHPILOT_ALLOWED_REPOSITORIES |
Comma-separated allowlists. Empty means unrestricted, which is only sensible for local use. |
PATCHPILOT_TRIGGER_RATE_LIMIT_*, PATCHPILOT_REJECTED_TRIGGER_QUARANTINE_* |
Abuse controls, on by default. |
PATCHPILOT_MODEL_TRIGGER_CLASSIFICATION_ENABLED |
Optionally ask the model whether a request is concrete enough before creating a task. |
PATCHPILOT_REVIEW_APPROVAL_ALLOWED_OPERATORS |
Who may approve a PENDING_REVIEW task. |
PATCHPILOT_DASHBOARD_BASE_URL |
Optional dashboard URL linked from issue comments and PR bodies. |
Profiles: docker (Compose, MySQL), local (MySQL on 127.0.0.1:3307, for running the backend from an IDE against the Compose database), and default (no database, in-memory stores, for quick local runs and tests).
The dashboard is an operations view: status counts and metrics, the task list with filters and search, task detail (timeline, generated diff, verification output, tool calls, model calls with token usage and latency), cancel / retry / approve-review actions, a manual task form with a read-only trigger dry run, queue and worker health, recent webhook deliveries, rejected triggers and quarantines, configuration and credential readiness, and the supported adapters.
Everything the dashboard shows comes from the JSON API under /api, for example:
H="X-PatchPilot-Admin-Token: $PATCHPILOT_ADMIN_TOKEN"
curl -H "$H" "http://127.0.0.1:8080/api/tasks?status=FAILED&limit=20"
curl -H "$H" http://127.0.0.1:8080/api/tasks/{taskId}/detail
curl -H "$H" http://127.0.0.1:8080/api/tasks/{taskId}/test-runs
curl -H "$H" http://127.0.0.1:8080/api/task-queue/summary
curl -H "$H" http://127.0.0.1:8080/api/rejected-triggers
curl -H "$H" -X POST http://127.0.0.1:8080/api/tasks/{taskId}/approve-review \
-H "Content-Type: application/json" -d '{"operator":"you","reason":"diff reviewed"}'PatchPilot must not, and is tested to not:
- push to a default branch or merge pull requests;
- run any shell command produced by the model (verification commands come from the adapter allowlist, and the command guard also pins the exact
gitinvocations); - read or write outside the task workspace, or touch protected paths such as
.envfiles,.git,.github/workflows, package-manager credential files, and private keys; - continue to verification, commit, push, or PR creation after the diff risk gate or the model patch review rejects the change;
- accept triggers from users or repositories outside the configured allowlists, or drop a rejected trigger without an audit record;
- log or echo secrets, including in issue comments and PR bodies.
A single Spring Boot process runs the webhook API, the queue poller, and the worker. See docs/architecture.md for the module map, the task state machine, and the data model.
PatchPilot/src/main/java/io/patchpilot/backend
github/ webhook controller + service, signature verifier, REST clients, credential probes
safety/ trigger policy, allowlists, rate limits, quarantine, diff risk gate, audit records
task/ FixTask model, MySQL/in-memory stores, queue poller, worker, WorkspaceFixTaskExecutor
workspace/ git clone / branch / recovery inside the workspace root, path guard
agent/ OpenAI-compatible client, FixPlan -> FileEditPlan -> PatchReview workflow, tools
language/ 15 build-system adapters and the adapter registry
runner/ verification runner, command allowlist, command execution guard
security/ admin token filter
configuration/ dashboard/ observability/
frontend/ React dashboard (Vite, Vitest)
docs/ architecture, code standard, decision log, demo fixture repositories
# backend tests (no database needed, about 10 s)
mvn -pl PatchPilot test
# backend without Docker: in-memory stores, port 8080
cd PatchPilot && ./mvnw spring-boot:run
# dashboard dev server on http://127.0.0.1:5173 (proxies /api to the backend)
cd frontend && npm install && npm run dev
# dashboard type-check, build, tests
cd frontend && npm run build && npm testIf you used an earlier PatchPilot database, recreate it with docker compose down -v; the migration set was renumbered when the project was reorganized.
- Authentication uses a personal access token. GitHub App installation tokens are in progress on a separate branch.
- The planner sees the issue text and the files it names; it does not yet search the repository tree, and a failed verification fails the task instead of feeding the output back for another attempt.
- Model-generated edits are limited to three whole files per task.
- Verification runs inside the backend container under the command allowlist; there is no per-task sandbox beyond the workspace guard.
- One process hosts both the API and the worker; splitting them is straightforward because the queue already lives in MySQL.
MIT, see LICENSE.
