patchpilot 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchpilot-0.1.0/.env.example +1 -0
- patchpilot-0.1.0/.gitignore +26 -0
- patchpilot-0.1.0/LICENSE +21 -0
- patchpilot-0.1.0/PKG-INFO +193 -0
- patchpilot-0.1.0/README.md +161 -0
- patchpilot-0.1.0/pyproject.toml +61 -0
- patchpilot-0.1.0/src/patchpilot/__init__.py +3 -0
- patchpilot-0.1.0/src/patchpilot/agent.py +90 -0
- patchpilot-0.1.0/src/patchpilot/analyze.py +36 -0
- patchpilot-0.1.0/src/patchpilot/cli.py +151 -0
- patchpilot-0.1.0/src/patchpilot/github.py +83 -0
- patchpilot-0.1.0/src/patchpilot/sandbox.py +114 -0
- patchpilot-0.1.0/src/patchpilot/tools.py +331 -0
- patchpilot-0.1.0/src/patchpilot/upgrade.py +287 -0
- patchpilot-0.1.0/tests/test_cli.py +31 -0
- patchpilot-0.1.0/tests/test_github.py +111 -0
- patchpilot-0.1.0/tests/test_sandbox.py +59 -0
- patchpilot-0.1.0/tests/test_tools.py +119 -0
- patchpilot-0.1.0/tests/test_upgrade.py +116 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ANTHROPIC_API_KEY=your-key-here
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Secrets
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Editors / OS
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
.DS_Store
|
|
24
|
+
|
|
25
|
+
# Agent workspaces
|
|
26
|
+
workspaces/
|
patchpilot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilmee De Silva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: patchpilot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An AI agent that upgrades your dependencies, fixes what breaks, and opens a PR
|
|
5
|
+
Project-URL: Homepage, https://github.com/ilmeedesilva/patchpilot
|
|
6
|
+
Project-URL: Issues, https://github.com/ilmeedesilva/patchpilot/issues
|
|
7
|
+
Project-URL: Demo, https://github.com/ilmeedesilva/patchpilot-demo
|
|
8
|
+
Author: Ilmee De Silva
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agent,claude,dependencies,developer-tools,llm,upgrade
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: anthropic>=2.3.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.17; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=6; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# patchpilot
|
|
34
|
+
|
|
35
|
+
[](https://github.com/ilmeedesilva/patchpilot/actions/workflows/ci.yml)
|
|
36
|
+

|
|
37
|
+
[](https://github.com/ilmeedesilva/patchpilot/blob/main/LICENSE)
|
|
38
|
+
|
|
39
|
+
**An AI agent that upgrades a dependency, fixes the code that breaks, and opens a pull request explaining what changed and why.**
|
|
40
|
+
|
|
41
|
+
Dependabot and Renovate bump version numbers. When the new version breaks your build, the fix is still on you. patchpilot does that part: it upgrades the package inside a sandbox, runs your tests, reads the failures and the changelog, edits your code until the tests pass, and hands you a reviewable PR.
|
|
42
|
+
|
|
43
|
+
<!-- TODO: demo GIF here: `patchpilot upgrade . pydantic --pr` → PR appears -->
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
$ patchpilot upgrade . pydantic --pr
|
|
47
|
+
• Creating an isolated copy of the repo on branch patchpilot/upgrade-pydantic
|
|
48
|
+
• Starting a Docker sandbox (python:3.13-slim) and installing the project
|
|
49
|
+
• Running the tests before upgrading
|
|
50
|
+
• Agent is upgrading pydantic
|
|
51
|
+
→ read_dependencies {}
|
|
52
|
+
→ get_latest_version {"package": "pydantic"}
|
|
53
|
+
→ pip_install {"requirement": "pydantic==2.14.1"}
|
|
54
|
+
→ run_tests {} # fails: BaseSettings moved out of pydantic
|
|
55
|
+
→ pip_install {"requirement": "pydantic-settings==2.16.2"}
|
|
56
|
+
→ write_file {"path": "app/config.py"}
|
|
57
|
+
→ write_file {"path": "app/models.py"}
|
|
58
|
+
→ run_tests {} # passes
|
|
59
|
+
• Re-running the tests to verify the agent's work
|
|
60
|
+
• Pull request: https://github.com/<you>/<repo>/pull/1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The PR description is written by the agent, and includes:
|
|
64
|
+
|
|
65
|
+
> **Breaking changes**
|
|
66
|
+
> - **`BaseSettings` moved to `pydantic-settings`**: importing it from `pydantic` now fails. Installed `pydantic-settings`, added it to `requirements.txt`, and now import from `pydantic_settings`.
|
|
67
|
+
> - **`@root_validator` without `skip_on_failure=True` is an error in v2**: replaced with `@model_validator(mode="after")`.
|
|
68
|
+
> - …
|
|
69
|
+
>
|
|
70
|
+
> **Needs your attention**
|
|
71
|
+
> - In v1 the password check ran even when another field had failed; in v2 it only runs once all fields are valid, so a bad email plus mismatched passwords now reports only the email error.
|
|
72
|
+
|
|
73
|
+
*(Trimmed output and report from a real run on [`examples/pydantic-demo`](https://github.com/ilmeedesilva/patchpilot/tree/main/examples/pydantic-demo).)*
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
You need Python 3.11+, [Docker Desktop](https://www.docker.com/products/docker-desktop/) running, and an [Anthropic API key](https://console.anthropic.com/settings/keys).
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pipx install git+https://github.com/ilmeedesilva/patchpilot.git # soon: pipx install patchpilot
|
|
81
|
+
export ANTHROPIC_API_KEY=sk-ant-... # or put it in a .env file where you run patchpilot
|
|
82
|
+
|
|
83
|
+
cd your-project # a git repo with a test suite
|
|
84
|
+
patchpilot analyze . # read-only report: what's outdated, what's risky
|
|
85
|
+
patchpilot upgrade . pydantic # upgrade + fix, committed to a new branch
|
|
86
|
+
patchpilot upgrade . pydantic --pr # ...and open a pull request
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each run uses Anthropic API credit. The amount depends on the size of the project and how much breaks.
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
### `patchpilot analyze <project>`
|
|
94
|
+
|
|
95
|
+
Reads your dependency files, checks PyPI for the latest versions and runs your tests once, then prints a Markdown report. It covers what's outdated, which upgrades are major-version jumps likely to need code changes, and a suggested order. It changes nothing.
|
|
96
|
+
|
|
97
|
+
### `patchpilot upgrade <project> <package>`
|
|
98
|
+
|
|
99
|
+
1. Checks that `<package>` really is a dependency of `<project>`. If it's declared in a subfolder instead, it tells you which one.
|
|
100
|
+
2. Makes a separate checkout of your repo (a git worktree) on a new branch, `patchpilot/upgrade-<package>`. **Your working copy is never touched.**
|
|
101
|
+
3. Starts a Docker sandbox, installs your project into it, and checks that the tests pass *before* changing anything.
|
|
102
|
+
4. Lets the agent work: upgrade the package, run the tests, read the failures and changelog, edit the code, and repeat.
|
|
103
|
+
5. **Re-runs the tests itself** instead of trusting the agent's word, then commits to the branch with the agent's report as the commit message.
|
|
104
|
+
|
|
105
|
+
Review the result with `git diff HEAD...patchpilot/upgrade-<package>`, then merge it or delete it.
|
|
106
|
+
|
|
107
|
+
| Option | |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `--pr` | Push the branch and open a GitHub pull request. If the tests still fail, it's opened as a **draft**. Uses the [GitHub CLI](https://cli.github.com/), so patchpilot never handles a GitHub token. |
|
|
110
|
+
| `--test-command "..."` | How to run your tests (default: `python -m pytest -q`). |
|
|
111
|
+
| `--image python:3.11-slim` | Docker image to run in. Match it to your project's Python version (default: `python:3.13-slim`). |
|
|
112
|
+
| `--no-docker` | Use a local virtualenv instead of Docker. Faster, but your project's code runs directly on your machine. |
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
```mermaid
|
|
117
|
+
flowchart LR
|
|
118
|
+
subgraph host["Your machine"]
|
|
119
|
+
CLI["patchpilot CLI"] --> Loop["Agent loop"]
|
|
120
|
+
Loop <-->|"tool calls ⇄ results"| Claude["Claude API"]
|
|
121
|
+
Loop --> Files["Read / edit files<br/>in a temporary worktree"]
|
|
122
|
+
end
|
|
123
|
+
subgraph sandbox["Docker sandbox: sees only the worktree"]
|
|
124
|
+
Pip["pip install <one package>"]
|
|
125
|
+
Tests["your test suite"]
|
|
126
|
+
end
|
|
127
|
+
Loop --> Pip
|
|
128
|
+
Loop --> Tests
|
|
129
|
+
Loop -->|"verify, commit"| Branch["patchpilot/upgrade-<pkg>"]
|
|
130
|
+
Branch -->|"--pr"| PR["GitHub pull request"]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
At the centre is a hand-written [tool-use loop](https://github.com/ilmeedesilva/patchpilot/blob/main/src/patchpilot/agent.py). patchpilot sends Claude the task and a list of tools. Claude replies with the tool it wants to call, patchpilot runs it and sends back the result, and this repeats until Claude writes its final report. Claude decides *what* to do next; patchpilot's code is the only thing that *does* anything.
|
|
134
|
+
|
|
135
|
+
| Tool | Runs where | What it does |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `read_dependencies` | host | Parses `requirements*.txt` and `pyproject.toml` |
|
|
138
|
+
| `get_latest_version` | host | Queries PyPI, including changelog URLs |
|
|
139
|
+
| `list_files`, `read_file` | host | Read-only, confined to the project folder |
|
|
140
|
+
| `edit_file`, `write_file` | host | Exact-match edits; refuses `.git/` and virtualenvs |
|
|
141
|
+
| `pip_install` | **sandbox** | One requirement at a time; flags, URLs and file paths are rejected |
|
|
142
|
+
| `run_tests` | **sandbox** | Your test command, with a timeout |
|
|
143
|
+
| `web_fetch` | Anthropic | Reads changelogs and migration guides |
|
|
144
|
+
|
|
145
|
+
### Safety
|
|
146
|
+
|
|
147
|
+
Upgrading means running code that isn't yours: your tests, and whatever a package runs when it installs. So that code runs inside a Docker container that:
|
|
148
|
+
|
|
149
|
+
- mounts **only the temporary worktree**, not your home folder, SSH keys or other projects
|
|
150
|
+
- runs as your user, not root, with all Linux capabilities dropped and `no-new-privileges`
|
|
151
|
+
- is capped at 2 CPUs, 2 GB of memory and 512 processes
|
|
152
|
+
- is deleted when the run ends, even if the run fails
|
|
153
|
+
|
|
154
|
+
The agent loop and your API key never enter the container. These properties are checked by [integration tests](https://github.com/ilmeedesilva/patchpilot/blob/main/tests/test_sandbox.py), not just described here.
|
|
155
|
+
|
|
156
|
+
### Design decisions
|
|
157
|
+
|
|
158
|
+
- **Verify, don't trust.** The agent says when it's done, but patchpilot re-runs the tests itself before committing. The ✅ in the output is patchpilot's verdict, not the model's.
|
|
159
|
+
- **Refuse to run when "tests pass" would mean nothing.** patchpilot stops if the tests already fail before the upgrade, or if the package isn't a dependency of the folder you pointed it at. Otherwise a green result could be meaningless.
|
|
160
|
+
- **Fail before spending, not after.** Docker, GitHub login, remotes and branch names are all checked before the first API call.
|
|
161
|
+
- **Leave nothing behind.** The worktree, container and virtualenv are cleaned up on success, failure or Ctrl-C. A branch only survives if it has a commit worth reviewing.
|
|
162
|
+
- **A small, explicit tool surface** instead of a general shell. It's easier to reason about what the agent can do, and easier to sandbox.
|
|
163
|
+
|
|
164
|
+
## Limitations
|
|
165
|
+
|
|
166
|
+
- Python projects only, with `requirements*.txt` or a PEP 621 `pyproject.toml` (no Poetry or uv lockfiles yet).
|
|
167
|
+
- One package per run.
|
|
168
|
+
- The fix is only as good as your tests. If they don't cover what changed, patchpilot can't catch it, though the agent flags behaviour changes it notices under **Needs your attention**.
|
|
169
|
+
- The sandbox needs network access to install packages, so code inside it can reach the internet.
|
|
170
|
+
|
|
171
|
+
## Roadmap
|
|
172
|
+
|
|
173
|
+
- [ ] Publish to PyPI (`pipx install patchpilot`)
|
|
174
|
+
- [ ] GitHub Action: run weekly and open PRs automatically
|
|
175
|
+
- [ ] Upgrade several packages in one run, ordered by risk
|
|
176
|
+
- [ ] Turn off the sandbox's network access while tests run
|
|
177
|
+
- [ ] uv and Poetry lockfile support
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git clone git@github.com:ilmeedesilva/patchpilot.git && cd patchpilot
|
|
183
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
184
|
+
pip install -e ".[dev]"
|
|
185
|
+
pytest # Docker tests are skipped if Docker isn't running
|
|
186
|
+
ruff check . && ruff format --check .
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The test suite doesn't call the Anthropic API: the agent is replaced with a fake that makes known edits, and GitHub with a local bare repo plus a stand-in `gh`.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
[MIT](https://github.com/ilmeedesilva/patchpilot/blob/main/LICENSE)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# patchpilot
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ilmeedesilva/patchpilot/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+
[](https://github.com/ilmeedesilva/patchpilot/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**An AI agent that upgrades a dependency, fixes the code that breaks, and opens a pull request explaining what changed and why.**
|
|
8
|
+
|
|
9
|
+
Dependabot and Renovate bump version numbers. When the new version breaks your build, the fix is still on you. patchpilot does that part: it upgrades the package inside a sandbox, runs your tests, reads the failures and the changelog, edits your code until the tests pass, and hands you a reviewable PR.
|
|
10
|
+
|
|
11
|
+
<!-- TODO: demo GIF here: `patchpilot upgrade . pydantic --pr` → PR appears -->
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
$ patchpilot upgrade . pydantic --pr
|
|
15
|
+
• Creating an isolated copy of the repo on branch patchpilot/upgrade-pydantic
|
|
16
|
+
• Starting a Docker sandbox (python:3.13-slim) and installing the project
|
|
17
|
+
• Running the tests before upgrading
|
|
18
|
+
• Agent is upgrading pydantic
|
|
19
|
+
→ read_dependencies {}
|
|
20
|
+
→ get_latest_version {"package": "pydantic"}
|
|
21
|
+
→ pip_install {"requirement": "pydantic==2.14.1"}
|
|
22
|
+
→ run_tests {} # fails: BaseSettings moved out of pydantic
|
|
23
|
+
→ pip_install {"requirement": "pydantic-settings==2.16.2"}
|
|
24
|
+
→ write_file {"path": "app/config.py"}
|
|
25
|
+
→ write_file {"path": "app/models.py"}
|
|
26
|
+
→ run_tests {} # passes
|
|
27
|
+
• Re-running the tests to verify the agent's work
|
|
28
|
+
• Pull request: https://github.com/<you>/<repo>/pull/1
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The PR description is written by the agent, and includes:
|
|
32
|
+
|
|
33
|
+
> **Breaking changes**
|
|
34
|
+
> - **`BaseSettings` moved to `pydantic-settings`**: importing it from `pydantic` now fails. Installed `pydantic-settings`, added it to `requirements.txt`, and now import from `pydantic_settings`.
|
|
35
|
+
> - **`@root_validator` without `skip_on_failure=True` is an error in v2**: replaced with `@model_validator(mode="after")`.
|
|
36
|
+
> - …
|
|
37
|
+
>
|
|
38
|
+
> **Needs your attention**
|
|
39
|
+
> - In v1 the password check ran even when another field had failed; in v2 it only runs once all fields are valid, so a bad email plus mismatched passwords now reports only the email error.
|
|
40
|
+
|
|
41
|
+
*(Trimmed output and report from a real run on [`examples/pydantic-demo`](https://github.com/ilmeedesilva/patchpilot/tree/main/examples/pydantic-demo).)*
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
You need Python 3.11+, [Docker Desktop](https://www.docker.com/products/docker-desktop/) running, and an [Anthropic API key](https://console.anthropic.com/settings/keys).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pipx install git+https://github.com/ilmeedesilva/patchpilot.git # soon: pipx install patchpilot
|
|
49
|
+
export ANTHROPIC_API_KEY=sk-ant-... # or put it in a .env file where you run patchpilot
|
|
50
|
+
|
|
51
|
+
cd your-project # a git repo with a test suite
|
|
52
|
+
patchpilot analyze . # read-only report: what's outdated, what's risky
|
|
53
|
+
patchpilot upgrade . pydantic # upgrade + fix, committed to a new branch
|
|
54
|
+
patchpilot upgrade . pydantic --pr # ...and open a pull request
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each run uses Anthropic API credit. The amount depends on the size of the project and how much breaks.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### `patchpilot analyze <project>`
|
|
62
|
+
|
|
63
|
+
Reads your dependency files, checks PyPI for the latest versions and runs your tests once, then prints a Markdown report. It covers what's outdated, which upgrades are major-version jumps likely to need code changes, and a suggested order. It changes nothing.
|
|
64
|
+
|
|
65
|
+
### `patchpilot upgrade <project> <package>`
|
|
66
|
+
|
|
67
|
+
1. Checks that `<package>` really is a dependency of `<project>`. If it's declared in a subfolder instead, it tells you which one.
|
|
68
|
+
2. Makes a separate checkout of your repo (a git worktree) on a new branch, `patchpilot/upgrade-<package>`. **Your working copy is never touched.**
|
|
69
|
+
3. Starts a Docker sandbox, installs your project into it, and checks that the tests pass *before* changing anything.
|
|
70
|
+
4. Lets the agent work: upgrade the package, run the tests, read the failures and changelog, edit the code, and repeat.
|
|
71
|
+
5. **Re-runs the tests itself** instead of trusting the agent's word, then commits to the branch with the agent's report as the commit message.
|
|
72
|
+
|
|
73
|
+
Review the result with `git diff HEAD...patchpilot/upgrade-<package>`, then merge it or delete it.
|
|
74
|
+
|
|
75
|
+
| Option | |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `--pr` | Push the branch and open a GitHub pull request. If the tests still fail, it's opened as a **draft**. Uses the [GitHub CLI](https://cli.github.com/), so patchpilot never handles a GitHub token. |
|
|
78
|
+
| `--test-command "..."` | How to run your tests (default: `python -m pytest -q`). |
|
|
79
|
+
| `--image python:3.11-slim` | Docker image to run in. Match it to your project's Python version (default: `python:3.13-slim`). |
|
|
80
|
+
| `--no-docker` | Use a local virtualenv instead of Docker. Faster, but your project's code runs directly on your machine. |
|
|
81
|
+
|
|
82
|
+
## How it works
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
subgraph host["Your machine"]
|
|
87
|
+
CLI["patchpilot CLI"] --> Loop["Agent loop"]
|
|
88
|
+
Loop <-->|"tool calls ⇄ results"| Claude["Claude API"]
|
|
89
|
+
Loop --> Files["Read / edit files<br/>in a temporary worktree"]
|
|
90
|
+
end
|
|
91
|
+
subgraph sandbox["Docker sandbox: sees only the worktree"]
|
|
92
|
+
Pip["pip install <one package>"]
|
|
93
|
+
Tests["your test suite"]
|
|
94
|
+
end
|
|
95
|
+
Loop --> Pip
|
|
96
|
+
Loop --> Tests
|
|
97
|
+
Loop -->|"verify, commit"| Branch["patchpilot/upgrade-<pkg>"]
|
|
98
|
+
Branch -->|"--pr"| PR["GitHub pull request"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
At the centre is a hand-written [tool-use loop](https://github.com/ilmeedesilva/patchpilot/blob/main/src/patchpilot/agent.py). patchpilot sends Claude the task and a list of tools. Claude replies with the tool it wants to call, patchpilot runs it and sends back the result, and this repeats until Claude writes its final report. Claude decides *what* to do next; patchpilot's code is the only thing that *does* anything.
|
|
102
|
+
|
|
103
|
+
| Tool | Runs where | What it does |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `read_dependencies` | host | Parses `requirements*.txt` and `pyproject.toml` |
|
|
106
|
+
| `get_latest_version` | host | Queries PyPI, including changelog URLs |
|
|
107
|
+
| `list_files`, `read_file` | host | Read-only, confined to the project folder |
|
|
108
|
+
| `edit_file`, `write_file` | host | Exact-match edits; refuses `.git/` and virtualenvs |
|
|
109
|
+
| `pip_install` | **sandbox** | One requirement at a time; flags, URLs and file paths are rejected |
|
|
110
|
+
| `run_tests` | **sandbox** | Your test command, with a timeout |
|
|
111
|
+
| `web_fetch` | Anthropic | Reads changelogs and migration guides |
|
|
112
|
+
|
|
113
|
+
### Safety
|
|
114
|
+
|
|
115
|
+
Upgrading means running code that isn't yours: your tests, and whatever a package runs when it installs. So that code runs inside a Docker container that:
|
|
116
|
+
|
|
117
|
+
- mounts **only the temporary worktree**, not your home folder, SSH keys or other projects
|
|
118
|
+
- runs as your user, not root, with all Linux capabilities dropped and `no-new-privileges`
|
|
119
|
+
- is capped at 2 CPUs, 2 GB of memory and 512 processes
|
|
120
|
+
- is deleted when the run ends, even if the run fails
|
|
121
|
+
|
|
122
|
+
The agent loop and your API key never enter the container. These properties are checked by [integration tests](https://github.com/ilmeedesilva/patchpilot/blob/main/tests/test_sandbox.py), not just described here.
|
|
123
|
+
|
|
124
|
+
### Design decisions
|
|
125
|
+
|
|
126
|
+
- **Verify, don't trust.** The agent says when it's done, but patchpilot re-runs the tests itself before committing. The ✅ in the output is patchpilot's verdict, not the model's.
|
|
127
|
+
- **Refuse to run when "tests pass" would mean nothing.** patchpilot stops if the tests already fail before the upgrade, or if the package isn't a dependency of the folder you pointed it at. Otherwise a green result could be meaningless.
|
|
128
|
+
- **Fail before spending, not after.** Docker, GitHub login, remotes and branch names are all checked before the first API call.
|
|
129
|
+
- **Leave nothing behind.** The worktree, container and virtualenv are cleaned up on success, failure or Ctrl-C. A branch only survives if it has a commit worth reviewing.
|
|
130
|
+
- **A small, explicit tool surface** instead of a general shell. It's easier to reason about what the agent can do, and easier to sandbox.
|
|
131
|
+
|
|
132
|
+
## Limitations
|
|
133
|
+
|
|
134
|
+
- Python projects only, with `requirements*.txt` or a PEP 621 `pyproject.toml` (no Poetry or uv lockfiles yet).
|
|
135
|
+
- One package per run.
|
|
136
|
+
- The fix is only as good as your tests. If they don't cover what changed, patchpilot can't catch it, though the agent flags behaviour changes it notices under **Needs your attention**.
|
|
137
|
+
- The sandbox needs network access to install packages, so code inside it can reach the internet.
|
|
138
|
+
|
|
139
|
+
## Roadmap
|
|
140
|
+
|
|
141
|
+
- [ ] Publish to PyPI (`pipx install patchpilot`)
|
|
142
|
+
- [ ] GitHub Action: run weekly and open PRs automatically
|
|
143
|
+
- [ ] Upgrade several packages in one run, ordered by risk
|
|
144
|
+
- [ ] Turn off the sandbox's network access while tests run
|
|
145
|
+
- [ ] uv and Poetry lockfile support
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone git@github.com:ilmeedesilva/patchpilot.git && cd patchpilot
|
|
151
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest # Docker tests are skipped if Docker isn't running
|
|
154
|
+
ruff check . && ruff format --check .
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The test suite doesn't call the Anthropic API: the agent is replaced with a fake that makes known edits, and GitHub with a local bare repo plus a stand-in `gh`.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
[MIT](https://github.com/ilmeedesilva/patchpilot/blob/main/LICENSE)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "patchpilot"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "An AI agent that upgrades your dependencies, fixes what breaks, and opens a PR"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Ilmee De Silva" }]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
keywords = ["ai-agent", "dependencies", "upgrade", "llm", "claude", "developer-tools"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Build Tools",
|
|
26
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"anthropic>=2.3.0",
|
|
30
|
+
"python-dotenv>=1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=9", "ruff>=0.17", "build>=1.2", "twine>=6"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/ilmeedesilva/patchpilot"
|
|
38
|
+
Issues = "https://github.com/ilmeedesilva/patchpilot/issues"
|
|
39
|
+
Demo = "https://github.com/ilmeedesilva/patchpilot-demo"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
patchpilot = "patchpilot.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version]
|
|
45
|
+
path = "src/patchpilot/__init__.py"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
exclude = ["examples", ".github"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/patchpilot"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The agent loop: send the conversation to Claude, run the tools it asks for, repeat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
import anthropic
|
|
8
|
+
|
|
9
|
+
from patchpilot.tools import TOOL_FUNCTIONS, ToolError, Workspace
|
|
10
|
+
|
|
11
|
+
MODEL = "claude-opus-5-5"
|
|
12
|
+
|
|
13
|
+
# Anthropic-hosted tool: Claude fetches URLs (like changelogs) itself, no code needed on our side.
|
|
14
|
+
# It only fetches URLs that already appear in the conversation, e.g. from get_latest_version.
|
|
15
|
+
WEB_FETCH_TOOL = {"type": "web_fetch_20260209", "name": "web_fetch", "max_uses": 8}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_tool(ws: Workspace, name: str, tool_input: dict) -> tuple[str, bool]:
|
|
19
|
+
"""Run one tool. Returns (result_text, is_error)."""
|
|
20
|
+
func = TOOL_FUNCTIONS.get(name)
|
|
21
|
+
if func is None:
|
|
22
|
+
return f"Unknown tool '{name}'.", True
|
|
23
|
+
try:
|
|
24
|
+
return func(ws, **tool_input), False
|
|
25
|
+
except ToolError as e:
|
|
26
|
+
return str(e), True
|
|
27
|
+
except TypeError as e:
|
|
28
|
+
return f"Invalid arguments for '{name}': {e}", True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_agent(
|
|
32
|
+
ws: Workspace,
|
|
33
|
+
system: str,
|
|
34
|
+
task: str,
|
|
35
|
+
tools: list[dict],
|
|
36
|
+
max_turns: int = 30,
|
|
37
|
+
client: anthropic.Anthropic | None = None,
|
|
38
|
+
on_tool_call: Callable[[str, dict], None] | None = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Run the agent until it finishes and return its final text."""
|
|
41
|
+
client = client or anthropic.Anthropic()
|
|
42
|
+
allowed = {t["name"] for t in tools if "input_schema" in t}
|
|
43
|
+
messages: list[dict] = [{"role": "user", "content": task}]
|
|
44
|
+
|
|
45
|
+
for _ in range(max_turns):
|
|
46
|
+
response = client.beta.messages.create(
|
|
47
|
+
model=MODEL,
|
|
48
|
+
max_tokens=16000,
|
|
49
|
+
system=system,
|
|
50
|
+
tools=tools,
|
|
51
|
+
messages=messages,
|
|
52
|
+
output_config={"effort": "high"},
|
|
53
|
+
# If a request is declined by a safety classifier, retry on the server's default
|
|
54
|
+
# fallback model instead of failing outright.
|
|
55
|
+
betas=["server-side-fallback-2026-07-01"],
|
|
56
|
+
fallbacks="default",
|
|
57
|
+
)
|
|
58
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
59
|
+
|
|
60
|
+
if response.stop_reason == "refusal":
|
|
61
|
+
raise RuntimeError("Claude declined this request.")
|
|
62
|
+
if response.stop_reason == "max_tokens":
|
|
63
|
+
raise RuntimeError("Response hit max_tokens before finishing.")
|
|
64
|
+
if response.stop_reason == "pause_turn":
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
tool_uses = [b for b in response.content if b.type == "tool_use"]
|
|
68
|
+
if not tool_uses:
|
|
69
|
+
return "\n".join(b.text for b in response.content if b.type == "text")
|
|
70
|
+
|
|
71
|
+
tool_results = []
|
|
72
|
+
for block in tool_uses:
|
|
73
|
+
if on_tool_call:
|
|
74
|
+
on_tool_call(block.name, block.input)
|
|
75
|
+
if block.name in allowed:
|
|
76
|
+
result, is_error = execute_tool(ws, block.name, block.input)
|
|
77
|
+
else:
|
|
78
|
+
result, is_error = f"Tool '{block.name}' is not available here.", True
|
|
79
|
+
tool_results.append(
|
|
80
|
+
{
|
|
81
|
+
"type": "tool_result",
|
|
82
|
+
"tool_use_id": block.id,
|
|
83
|
+
"content": result,
|
|
84
|
+
"is_error": is_error,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
# All results for one turn go back in a single user message.
|
|
88
|
+
messages.append({"role": "user", "content": tool_results})
|
|
89
|
+
|
|
90
|
+
raise RuntimeError(f"Agent did not finish within {max_turns} turns.")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""`patchpilot analyze`: a read-only report of what could be upgraded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from patchpilot.agent import run_agent
|
|
8
|
+
from patchpilot.tools import READ_TOOL_SCHEMAS, Workspace
|
|
9
|
+
|
|
10
|
+
SYSTEM_PROMPT = """\
|
|
11
|
+
You are patchpilot, an agent that keeps a Python project's dependencies up to date.
|
|
12
|
+
|
|
13
|
+
Your job right now is to produce an upgrade report. Use the tools to:
|
|
14
|
+
1. Read the project's declared dependencies.
|
|
15
|
+
2. Look up the latest version of each one on PyPI.
|
|
16
|
+
3. Run the test suite to establish a baseline.
|
|
17
|
+
|
|
18
|
+
Then finish with a concise report in Markdown:
|
|
19
|
+
- A table of dependencies: name, current specifier, latest version, and whether the
|
|
20
|
+
current specifier already allows the latest version.
|
|
21
|
+
- Which upgrades look like major-version jumps that are likely to need code changes.
|
|
22
|
+
- The baseline test result.
|
|
23
|
+
- A recommended order to upgrade in, with one line of reasoning each.
|
|
24
|
+
|
|
25
|
+
You can only read and run tests in this phase; do not suggest that you changed any files.\
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def analyze(ws: Workspace, on_tool_call: Callable[[str, dict], None] | None = None) -> str:
|
|
30
|
+
return run_agent(
|
|
31
|
+
ws,
|
|
32
|
+
system=SYSTEM_PROMPT,
|
|
33
|
+
task=f"Analyze the project at {ws.root} and write the report.",
|
|
34
|
+
tools=READ_TOOL_SCHEMAS,
|
|
35
|
+
on_tool_call=on_tool_call,
|
|
36
|
+
)
|