<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilmee De Silva</title>
    <description>The latest articles on DEV Community by Ilmee De Silva (@ilmeedesilva).</description>
    <link>https://dev.to/ilmeedesilva</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4214784%2Fbbe5e0db-7f6d-4ae2-bdd3-af86f0f07c03.jpg</url>
      <title>DEV Community: Ilmee De Silva</title>
      <link>https://dev.to/ilmeedesilva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilmeedesilva"/>
    <language>en</language>
    <item>
      <title>My AI agent now opens its own pull requests every Monday</title>
      <dc:creator>Ilmee De Silva</dc:creator>
      <pubDate>Tue, 03 Nov 2026 02:09:56 +0000</pubDate>
      <link>https://dev.to/ilmeedesilva/my-ai-agent-now-opens-its-own-pull-requests-every-monday-4fji</link>
      <guid>https://dev.to/ilmeedesilva/my-ai-agent-now-opens-its-own-pull-requests-every-monday-4fji</guid>
      <description>&lt;p&gt;A few days ago I wrote about &lt;strong&gt;&lt;a href="https://github.com/ilmeedesilva/patchpilot" rel="noopener noreferrer"&gt;patchpilot&lt;/a&gt;&lt;/strong&gt;, an agent that upgrades a Python dependency and fixes the code the new version breaks (&lt;a href="https://dev.to/ilmeedesilva/i-built-an-ai-agent-that-fixes-the-code-a-dependency-upgrade-breaks-2664"&gt;part 1 is here&lt;/a&gt;). It worked, but it had one honest flaw: &lt;strong&gt;you had to remember to run it.&lt;/strong&gt; And "I'll do the upgrade later" is exactly how dependencies end up two major versions behind.&lt;/p&gt;

&lt;p&gt;So I made patchpilot a GitHub Action. You add one workflow file, and every Monday it picks an outdated package, upgrades it in a sandbox, fixes the code until the tests pass, and opens a pull request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60n54qte88xnqqakcxgw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60n54qte88xnqqakcxgw.gif" alt="The patchpilot Action picking marshmallow, upgrading it, and opening a pull request" width="799" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;That's the log from its first real run, replayed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened in that run, with nobody at a keyboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;skipped pydantic, which already had an open patchpilot PR&lt;/li&gt;
&lt;li&gt;picked &lt;strong&gt;marshmallow 3 → 4&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;found that version 4 removed the &lt;code&gt;missing=&lt;/code&gt; and &lt;code&gt;default=&lt;/code&gt; field options, and the test module crashed on import&lt;/li&gt;
&lt;li&gt;renamed them to &lt;code&gt;load_default=&lt;/code&gt; and &lt;code&gt;dump_default=&lt;/code&gt;, and all 9 tests passed&lt;/li&gt;
&lt;li&gt;opened &lt;strong&gt;&lt;a href="https://github.com/ilmeedesilva/patchpilot-demo/pull/3" rel="noopener noreferrer"&gt;this pull request&lt;/a&gt;&lt;/strong&gt; as &lt;code&gt;github-actions[bot]&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also flagged a README line that still said "marshmallow 3", and deliberately left it alone to keep the PR focused. That's the judgement I'd want from a human reviewer.&lt;/p&gt;

&lt;p&gt;Here's the whole setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patchpilot&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;  &lt;span class="c1"&gt;# every Monday at 06:00 UTC&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# adds a "Run workflow" button&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ilmeedesilva/patchpilot@v0.2.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;anthropic-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's on the &lt;a href="https://github.com/marketplace/actions/patchpilot-dependency-upgrader" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt;. Getting there took more decisions than I expected. These are the ones worth sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't use AI for the boring decision
&lt;/h2&gt;

&lt;p&gt;The first question is &lt;em&gt;which&lt;/em&gt; package to upgrade. I could have asked the model. I didn't, because this decision has a clear right answer, and code gives it for free, instantly, and the same way every time.&lt;/p&gt;

&lt;p&gt;patchpilot's rule: a package is a candidate when its &lt;strong&gt;newest release on PyPI falls outside the range you declared&lt;/strong&gt;. &lt;code&gt;pydantic&amp;gt;=1.10,&amp;lt;2&lt;/code&gt; while 2.x is out? Candidate. An unpinned &lt;code&gt;requests&lt;/code&gt;? Never, because pip already installs the newest version. Among the candidates it picks the &lt;strong&gt;smallest jump first&lt;/strong&gt; (patch, then minor, then major), because the safest upgrade is the one most likely to get merged.&lt;/p&gt;

&lt;p&gt;One rule makes the weekly schedule work: &lt;strong&gt;skip any package that already has a &lt;code&gt;patchpilot/upgrade-*&lt;/code&gt; branch.&lt;/strong&gt; Without it, every Monday would redo the same upgrade. There was a catch, though. On a GitHub runner, &lt;code&gt;actions/checkout&lt;/code&gt; fetches only the default branch, so a local branch check finds nothing. patchpilot asks the remote directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-remote &lt;span class="nt"&gt;--heads&lt;/span&gt; origin &lt;span class="s1"&gt;'patchpilot/upgrade-*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My rule of thumb now: &lt;strong&gt;use the model for the parts that need judgement&lt;/strong&gt; (reading a changelog, fixing code). &lt;strong&gt;Use code for everything else.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Quiet weeks should cost nothing
&lt;/h2&gt;

&lt;p&gt;A scheduled job that spends money every week to find out there's nothing to do is a job people switch off. So when every outdated package already has an open PR, patchpilot prints "Nothing to upgrade" and &lt;strong&gt;exits successfully, without a single API call&lt;/strong&gt;. The run takes about a second.&lt;/p&gt;

&lt;p&gt;Exit code 0 matters here too. If "nothing to do" were an error, your Actions tab would turn red every quiet week, and people learn to ignore red.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A throwaway machine is not a sandbox
&lt;/h2&gt;

&lt;p&gt;GitHub gives every job a fresh virtual machine that's deleted afterwards. So it was tempting to drop patchpilot's Docker sandbox in CI and run everything directly on the runner.&lt;/p&gt;

&lt;p&gt;But while the job runs, that machine holds &lt;strong&gt;your Anthropic API key and a GitHub token that can push to your repo&lt;/strong&gt;. Meanwhile it's installing a new version of a package and running a test suite: code you didn't write. A compromised release could read those secrets from the environment in one line.&lt;/p&gt;

&lt;p&gt;So the sandbox stays. The project's code runs in a container that sees only a temporary copy of the repo, and never the environment the secrets live in. "Throwaway" protects the &lt;em&gt;next&lt;/em&gt; job. It doesn't protect &lt;em&gt;this&lt;/em&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Small details that would have bitten users
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never paste inputs into shell scripts.&lt;/strong&gt; A composite action that writes &lt;code&gt;${{ inputs.package }}&lt;/code&gt; straight into &lt;code&gt;run:&lt;/code&gt; lets a crafted input run shell commands. Every input goes through an environment variable instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the tool to the action.&lt;/strong&gt; The action installs patchpilot from its own source (&lt;code&gt;pipx install "$GITHUB_ACTION_PATH"&lt;/code&gt;), not "whatever's latest on PyPI". So &lt;code&gt;@v0.2.0&lt;/code&gt; always means patchpilot 0.2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged in isn't the same as &lt;code&gt;gh auth status&lt;/code&gt;.&lt;/strong&gt; Locally, patchpilot checks &lt;code&gt;gh auth status&lt;/code&gt; before doing any work. In Actions the token comes from an environment variable, and that check can fail even though pushing and opening PRs work fine. Now a token in &lt;code&gt;GH_TOKEN&lt;/code&gt; counts as logged in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the action in CI without a key.&lt;/strong&gt; CI runs the real action on a tiny project where nothing is outdated. That exercises install, picking and the exit code on GitHub's machines, without ever calling Claude.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. The demo taught me more than the code did
&lt;/h2&gt;

&lt;p&gt;To test it I needed a second outdated package in the demo repo, one that &lt;strong&gt;really&lt;/strong&gt; breaks. My first pick was &lt;code&gt;semver&lt;/code&gt;, since version 3 was supposed to remove the old module functions. Before building anything, I installed both versions and checked. Version 3 had &lt;em&gt;kept&lt;/em&gt; them, only deprecated. That demo would have been an upgrade where nothing broke, and nothing for patchpilot to fix. I switched to marshmallow 4, which really did remove &lt;code&gt;missing=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The other trap: adding marshmallow to &lt;code&gt;requirements.txt&lt;/code&gt; would have given my live pydantic PR a merge conflict, since that PR edits the same lines. It went into a separate &lt;code&gt;requirements-api.txt&lt;/code&gt; instead. Small, but a "live example" with a red conflict banner isn't much of an example.&lt;/p&gt;

&lt;p&gt;Even publishing had a lesson. The Marketplace rejected the name "patchpilot" because a GitHub account already has it. The failed release &lt;em&gt;still created the git tag&lt;/em&gt;, so it's worth checking for stray tags before you try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The next goal is the one that matters most: &lt;strong&gt;real users&lt;/strong&gt;. I'll run patchpilot on a few open-source projects with dependencies stuck behind an old major version, review every diff by hand, and &lt;strong&gt;ask the maintainers before opening anything&lt;/strong&gt;. A merged PR in someone else's project is a much better test than any demo.&lt;/p&gt;

&lt;p&gt;If you'd like to try it, the &lt;a href="https://github.com/ilmeedesilva/patchpilot#use-it-in-github-actions" rel="noopener noreferrer"&gt;README&lt;/a&gt; has the copy-paste setup. It needs an Anthropic API key, and you can still run it locally with &lt;code&gt;pipx install patchpilot&lt;/code&gt;. Feedback is very welcome, especially an upgrade you'd like to see it attempt.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm on &lt;a href="https://www.linkedin.com/in/ilmeedesilva" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; if you'd like to connect.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>githubactions</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built an AI agent that fixes the code a dependency upgrade breaks</title>
      <dc:creator>Ilmee De Silva</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:58:40 +0000</pubDate>
      <link>https://dev.to/ilmeedesilva/i-built-an-ai-agent-that-fixes-the-code-a-dependency-upgrade-breaks-2664</link>
      <guid>https://dev.to/ilmeedesilva/i-built-an-ai-agent-that-fixes-the-code-a-dependency-upgrade-breaks-2664</guid>
      <description>&lt;p&gt;Every Python developer knows this moment. Dependabot opens a PR, &lt;em&gt;"Bump pydantic from 1.10 to 2.14"&lt;/em&gt;, CI goes red, and the PR sits there for three months because fixing it is real work.&lt;/p&gt;

&lt;p&gt;Bumping the version number was never the hard part. Fixing what breaks is. So I built &lt;strong&gt;&lt;a href="https://github.com/ilmeedesilva/patchpilot" rel="noopener noreferrer"&gt;patchpilot&lt;/a&gt;&lt;/strong&gt;, an agent that does that part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;patchpilot upgrade &lt;span class="nb"&gt;.&lt;/span&gt; pydantic &lt;span class="nt"&gt;--pr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It upgrades the package inside a sandbox, runs your tests, reads the failures and the changelog, edits your code until the tests pass, and opens a pull request explaining what broke and how it was fixed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpv1tha2s9binpn6yb74p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpv1tha2s9binpn6yb74p.gif" alt="patchpilot upgrading pydantic 1 to 2 and opening a pull request" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a real run, sped up 2×. Here's &lt;a href="https://github.com/ilmeedesilva/patchpilot-demo/pull/1" rel="noopener noreferrer"&gt;the pull request it opened&lt;/a&gt;: the description, the diff and the verification note were all written by patchpilot.&lt;/p&gt;

&lt;p&gt;You can try it yourself with &lt;code&gt;pipx install patchpilot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent is just a loop
&lt;/h2&gt;

&lt;p&gt;Before this project, "AI agent" sounded like magic to me. It turned out to be a &lt;code&gt;for&lt;/code&gt; loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send the model the task and a list of tools it may use (&lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;run_tests&lt;/code&gt;, &lt;code&gt;pip_install&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;The model replies: &lt;em&gt;"call &lt;code&gt;run_tests&lt;/code&gt;"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;My code runs the tests and sends back the output.&lt;/li&gt;
&lt;li&gt;Repeat until the model replies with text instead of a tool call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model decides &lt;em&gt;what&lt;/em&gt; to do next. My code is the only thing that &lt;em&gt;does&lt;/em&gt; anything. That split made everything else easier to reason about, especially safety. I wrote the loop by hand instead of using a framework, so I'd understand every step. The whole file is under 100 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real run
&lt;/h2&gt;

&lt;p&gt;I wrote a small app against pydantic v1 that breaks three ways on v2, and pointed patchpilot at it. Without being told what was wrong, the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installed pydantic 2, ran the tests, and saw that they couldn't even load&lt;/li&gt;
&lt;li&gt;worked out that &lt;code&gt;BaseSettings&lt;/code&gt; had moved to a separate package, installed &lt;code&gt;pydantic-settings&lt;/code&gt;, and added it to &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;rewrote &lt;code&gt;@root_validator&lt;/code&gt; as &lt;code&gt;@model_validator(mode="after")&lt;/code&gt; and replaced the deprecated &lt;code&gt;.dict()&lt;/code&gt; and &lt;code&gt;.parse_obj()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;got all the tests passing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that impressed me most was in its report, under &lt;em&gt;"Needs your attention"&lt;/em&gt;. It pointed out a subtle behaviour change no test would catch: in v2, a form with a bad email &lt;em&gt;and&lt;/em&gt; mismatched passwords now reports only the email error. That's exactly what a careful human reviewer would mention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: verify, don't trust
&lt;/h2&gt;

&lt;p&gt;My favourite bug came from the second real run. I ran the upgrade from the repo root instead of the demo folder. The agent upgraded pydantic, ran "the tests"… which were patchpilot's own tests. They never import pydantic, so they passed before and after. patchpilot proudly printed &lt;strong&gt;✅ tests pass&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To its credit, the agent noticed and wrote in its report that the right tests were never run. But the green tick was still a lie. Two fixes came out of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patchpilot now &lt;strong&gt;re-runs the tests itself&lt;/strong&gt; after the agent says it's done. The ✅ is patchpilot's verdict, not the model's.&lt;/li&gt;
&lt;li&gt;patchpilot &lt;strong&gt;refuses to start&lt;/strong&gt; if the package isn't a dependency of the folder you pointed it at, and tells you which folder declares it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general lesson: when an agent's output gets acted on automatically, check its claims with code, not with more prompting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: the agent runs code you don't own
&lt;/h2&gt;

&lt;p&gt;Upgrading a package means installing it (and package installs can run arbitrary code) and running tests. If you point patchpilot at a random open-source project, that's a stranger's code on your laptop.&lt;/p&gt;

&lt;p&gt;So everything that &lt;em&gt;executes&lt;/em&gt; runs in a Docker container that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mounts only a temporary git worktree, not your home folder or SSH keys&lt;/li&gt;
&lt;li&gt;runs as a normal user with all Linux capabilities dropped&lt;/li&gt;
&lt;li&gt;is capped at 2 CPUs, 2 GB of RAM and 512 processes&lt;/li&gt;
&lt;li&gt;is deleted at the end of the run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent loop and the API key stay outside the container. I also wrote integration tests that &lt;em&gt;check&lt;/em&gt; these claims. For example, one asserts that the container can't see my home directory. Writing "it's sandboxed" in a README is easy; proving it is better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: fail before spending, not after
&lt;/h2&gt;

&lt;p&gt;An agent run costs money and minutes. Few things are more frustrating than watching it do five minutes of work and then fail at the last step because you weren't logged in to GitHub.&lt;/p&gt;

&lt;p&gt;So patchpilot checks everything it can &lt;strong&gt;before the first API call&lt;/strong&gt;: Docker is running, &lt;code&gt;gh&lt;/code&gt; is logged in, the repo has a remote, the base branch is pushed, and the branch name is free. The same idea showed up when my API credit ran out mid-test: the SDK error was a 40-line traceback. Now it's one line saying what happened and where to add credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: a small tool surface beats a shell
&lt;/h2&gt;

&lt;p&gt;The easy design is to give the model a &lt;code&gt;bash&lt;/code&gt; tool and let it do anything. I deliberately didn't. patchpilot's agent gets narrow tools: &lt;code&gt;pip_install&lt;/code&gt; takes exactly one requirement string and rejects flags, URLs and file paths, and &lt;code&gt;edit_file&lt;/code&gt; refuses to touch &lt;code&gt;.git/&lt;/code&gt;. Fewer tools mean fewer surprises, and they're much easier to sandbox and test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;a GitHub Action that runs weekly and opens PRs automatically&lt;/li&gt;
&lt;li&gt;upgrading several packages in one run, ordered by risk&lt;/li&gt;
&lt;li&gt;cutting the sandbox's network access while the tests run&lt;/li&gt;
&lt;li&gt;support for uv and Poetry lockfiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;patchpilot is open source and on PyPI (&lt;code&gt;pipx install patchpilot&lt;/code&gt;). The code is on GitHub at &lt;strong&gt;&lt;a href="https://github.com/ilmeedesilva/patchpilot" rel="noopener noreferrer"&gt;ilmeedesilva/patchpilot&lt;/a&gt;&lt;/strong&gt;, and &lt;a href="https://github.com/ilmeedesilva/patchpilot-demo/pull/1" rel="noopener noreferrer"&gt;here's the PR it opened&lt;/a&gt; in the demo above. Issues and ideas are welcome, especially breaking upgrades you'd like to see it try.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm on &lt;a href="https://www.linkedin.com/in/ilmeedesilva" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; if you'd like to connect.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
