I run about twenty production and support repos by myself — a content site with a curated data feed, a couple of scheduled scrapers, some Cloudflare Workers, and the usual pile of half-dormant tooling that accumulates around all of it. There is no ops team. Maintenance debt on a solo estate doesn't announce itself; it just compounds quietly until the day you need the thing you didn't maintain.
So I built a robot to do the maintenance, and — this is the part people react to — I gave it merge rights.
Every night at midnight, a GitHub Actions job wakes up, surveys every repo in my org, and heals what it can: opens pull requests, merges them itself, files issues for anything that needs a human, and writes me a report. I named the repo make-me-better, which tells you what I think of my own discipline.
Before you close the tab: it operates under a budget, a tier system, and a kill switch. Those three things are the whole article. The agent is the easy part.
The rules it lives under
The prompt gives it three tiers of allowed action:
- Tier 1 — mechanical. Scaffolding missing config, adding a memory directory, boilerplate that has one obviously correct form. It can open and merge these itself.
- Tier 2 — judgment. Anything touching business logic gets an issue, not a PR. The prompt's rule is "never guess at business logic."
- Tier 3 — self-modification. It's allowed to change its own code — observation script, workflow, even its own prompt. One per night, maximum.
On top of the tiers, hard caps: three heal PRs, three issues, one self-change per night. It never deletes anything and never force-pushes. And the kill switch is deliberately dumb: commit a file named PAUSE to the repo root and it stops. I didn't want a dashboard or a config flag whose location I'd have to remember at 2 a.m.
One more design decision that earned its keep immediately: the prompt is report-first. The report gets written and committed even if the run dies partway — the commit step runs if: ${{ !cancelled() }}. A run that crashes still tells you what it saw before it crashed. When you're debugging a robot that works while you sleep, partial evidence beats a silent grave.
Shakedown night: four failures, each one layer deeper
The first night was a tour of my own errors in judgment, peeled off one at a time.
Run 1 died on a shell bug I'd written in the observation script. I had this everywhere:
last_commit=$(gh api "repos/$OWNER/$r/commits?per_page=1" -q '...' 2>/dev/null || echo "")
Looks fine. Isn't. If gh api writes a partial JSON error body before dying — a 409 from an empty repo, say — the || echo appends the fallback to that partial output instead of replacing it. You get {"message":"Git Repository is empty."} concatenated with your fallback, which is neither valid JSON nor your fallback, and everything downstream chokes. The fix is to assign first and fall back only if the substitution itself failed:
last_commit=$(gh api "..." -q '...' 2>/dev/null) || last_commit=""
I have been writing shell scripts on and off since the nineties. I still shipped this. Three separate times, in the same file.
Run 2 failed on token exchange — the action wanted an OIDC flow I hadn't configured, fixed by passing my PAT explicitly.
Runs 3 and 4 were the interesting ones: the agent ran perfectly, wrote a full report, and then couldn't push it. 403. My fine-grained PAT had read-only access to nearly everything — I'd created it stingy on purpose and then forgotten I'd done that. And here's the error in judgment worth writing down: when the agent later tried to modify its own workflow file, I "knew" from years of classic GitHub tokens that it needed the workflow scope, and I went looking for one. Fine-grained PATs don't have a workflow scope. They gate workflow-file writes on a repository permission called Workflows — and editing a fine-grained PAT's permissions keeps the same token value, so nothing needs re-pasting anywhere. Twenty-five years of accumulated knowledge is a real asset, except for the parts that have silently expired.
The night it blew its own budget — and the refusal that made me trust it
Night one, the job also crashed into its turn ceiling six times, and GitHub dutifully restarted it. Each restart checked whether it was within its nightly caps the only way it could: by counting open heal PRs. But merged PRs drop off that list. So each incarnation saw a clean slate, and by morning the night's total was four heal PRs and two self-modifications — over both caps. Nothing it merged was wrong. The budget still failed at its one job, which was bounding a night, not a run. Caps that reset on crash aren't caps; they're suggestions.
What happened next is the reason this article exists. In its own report, the agent diagnosed the cap-tracking hole, wrote up the fix — count the night's PRs cumulatively via search, not the open list — and then declined to implement it, because its self-modification budget for the night was already spent. It flagged the fix and stopped.
A system that finds a flaw in its own governance and won't fix it without budget is a system whose budget means something.
Steady state: it queues its own work and does it
Three clean nights in, the loop looks like this: one night's report says "worth a future change: skip archived repos, they can't take PRs anyway." The next night's single self-modification slot goes to exactly that fix — validated with bash -n and merged. The report even tells the next run what to verify: that the four archived repos actually vanish from the survey.
Along the way it fixed a bug in my code that my own review pass had missed, caught that one of its metrics was silently measuring the wrong thing (the variable said "last memory update"; the API query said otherwise), and corrected its own backlog count downward when it discovered five of its "repos needing work" were archived or deprecated — overcounting it had itself introduced the night before. It flags its own bad data. Most humans I've managed needed coaching to do that.
The failure mode nobody warned me about
Within days, eight of my local clones were behind their remotes. Of course they were: the robot commits to GitHub every night and has no idea my laptop exists. Every workflow habit I had assumed I was the only writer. Now there's a second author who never sleeps, and git pull before touching anything has gone from hygiene to survival. If you build one of these, tell your team — even if, like me, your team is you.
What to steal
- Caps per night, not per run — and count them from a source that survives crashes and restarts.
- Tier the autonomy. Mechanical changes get merge rights. Judgment gets an issue. Self-modification gets a budget of one.
- Report-first. A dead run that left a partial report is debuggable. A dead run that left nothing is a séance.
-
A kill switch with no moving parts. A file named
PAUSE. You will not remember anything cleverer under stress. - Make it write the postmortem. The nightly report is where every bug in this article was actually caught — including its own.
The uncomfortable coda
One error in judgment doesn't have a diff attached. While I was building all this, three finished, paid pitches sat in my drafts folder for thirty-five days, ready to send. The automation was genuinely worth building — but I built it partly because polishing a robot is comfortable and asking a human for money is not. The estate now heals itself nightly. Nobody has built the agent that hits send for you, because that one you have to run yourself.
I'm a cloud and infrastructure architect — 25 years of enterprise work, now running a solo consultancy. If your team needs someone who has already seen the enterprise version of your problem, I'm at jennifer@jenatech.io.
Top comments (0)