Claude routines to plan and solve issues automatically
Some mornings I open GitHub and there are pull requests waiting for me that I did not write.
They are small, well-scoped changes. Each one closes an issue. Each one has a test that fails without the change and passes with it. Each one has already been reviewed — the build is green, the linter is quiet, and there is a comment explaining what was done and why. All I have to do is read them and click merge.
I wrote none of it. A handful of agents did, overnight, while I slept. They planned the work, implemented it, and reviewed each other, and they did it without me in the loop at any point. This is the story of how that pipeline is built — and the one idea that makes it work, which is far less clever than it sounds.
The shape of the thing
There are three agents, and each one runs on its own hourly schedule against a large monorepo I work on. They are deliberately dumb in isolation. Each wakes up, does exactly one job, writes down what it did, and goes back to sleep.
The planner looks for the highest-priority open issue that nobody has planned yet. It reads the issue, digs through the codebase to understand what the change actually involves, and writes a concrete implementation plan: which files to touch, which tests to add, what could go wrong. It posts that plan and marks the issue as planned.
The implementer looks for a planned issue that nobody is working on. It reads the plan, writes the failing test first, makes it pass, and runs the full test-build-lint gate on its own work before pushing a branch. Then it marks the issue as ready for review.
The reviewer looks for an issue that’s waiting for review. It checks out the branch, runs everything for real, and decides: if the work holds up, it opens a pull request; if it doesn’t, it kicks the issue back to the implementer with a list of what’s wrong.
That’s the whole system. Three roles, running on a clock, each blind to the others except for one thing: the trail they leave behind.
The real idea: the issue tracker is the memory
Here’s the part that took me a while to appreciate. When people imagine wiring up autonomous agents, they usually picture a big orchestrator: one long-running process that holds a plan in memory, calls the planner, hands its output to the implementer, waits, hands that to the reviewer, and keeps the whole state of the world in its head.
I did none of that. There is no orchestrator. The three agents never talk to each other directly, never share a process, never pass messages in memory. Everything they need to know lives in the issue tracker — in labels and in comments. The issue is the shared memory.
This sounds like a downgrade. It is the opposite. When the only durable state lives in a system you don’t control, every run has to be stateless: it wakes up, reconstructs everything it needs by reading the issue, does its work, and writes the result back where the next agent will find it. Nothing is held in memory between runs, because there is no “between runs.”
And that turns out to buy almost everything I care about:
- Crash safety. If an agent dies halfway — the machine sleeps, the process is killed, the API times out — nothing is corrupted. The issue is still in whatever state the last successful write left it. The next scheduled run just picks up from there.
- Observability for free. I don’t need a dashboard. The dashboard is GitHub. I can see exactly what each agent decided and why, because it left a comment. The state of the entire pipeline is a filter on my issue list.
- Human interruptibility. At any point I can grab an issue, relabel it, edit a plan, or take it out of the queue entirely — and the agents just respect the new state on their next pass. I’m not fighting an orchestrator; I’m editing the same shared board they read from.
The agents are stateless functions. The issue tracker is the state. That separation is the whole trick.
Labels as a state machine
If the issue holds the state, then something has to define the legal states and the moves between them. That’s what the labels are. Each issue moves through a small, explicit lifecycle, and the label is the current node:
stateDiagram-v2
[*] --> unplanned
unplanned --> planned: planner writes a plan
planned --> in_review: implementer pushes a branch
in_review --> pr_open: reviewer approves
in_review --> planned: reviewer requests changes
planned --> needs_human: too many failed rounds
pr_open --> [*]: human merges
needs_human --> [*]: human steps in
Each agent only ever acts on issues in one specific state, and its whole job is to move an issue one step along. The planner turns unplanned into planned. The implementer turns planned into in_review. The reviewer turns in_review into either pr_open or — if the work isn’t good enough — right back to planned, where the implementer will find it again and try to fix what was flagged.
Because the transitions are explicit, the pipeline self-heals. A rejected change doesn’t need a callback or a retry queue; it’s just an issue that’s back in the planned state, indistinguishable to the implementer from any other. The loop between implementer and reviewer can run as many times as it needs to, and I never have to wire up a single connection between them. They are connected only by the label.
Separation of powers
The three-way split isn’t just tidy. It’s the thing that makes the output trustworthy, and it’s borrowed directly from how human teams already work.
The planner can’t write code. It can only propose. This keeps planning honest — the agent that decides what to do isn’t the one under pressure to make the change look done.
The implementer can’t approve its own work. It runs the review gate on itself before pushing — a good habit — but passing your own check has never been the same as passing review. Its work still has to survive a separate, skeptical pass.
The reviewer assumes the work is wrong until proven otherwise. This is the agent I’m proudest of. It is deliberately adversarial. It doesn’t read the diff and nod along; it runs the tests, the build, and the linter itself, and it refuses to approve anything it couldn’t execute. Reading code is not reviewing code.
The sharpest thing it does is verify that the tests are real. It’s easy — for a human or a machine — to write a test that passes whether or not the feature works. So the reviewer strips the implementation back out, keeps only the newly added tests, and runs them. If they still pass without the code that’s supposed to make them pass, the tests are decorative, and the whole change is rejected:
# reviewer, checking that the tests actually test something
revert the implementation, keep the new tests
run the new tests
→ they MUST now fail.
→ if they still pass, the tests don't guard the behavior. Reject.
restore the implementation
This is just least-privilege and code review — the oldest ideas in software teams — pointed at agents instead of people. No single agent has the authority to both do the work and declare it done. Trust is earned through verification by a party that didn’t do the work.
What actually breaks
I’d be lying if I made this sound frictionless. The interesting failures are all in the seams.
Empty runs are the common case, and that’s fine. Most hours, an agent wakes up, finds nothing in its state to act on, and goes back to sleep. A pipeline that mostly does nothing is a healthy one — it means the work is keeping up with the backlog, not that anything is broken.
Labels are not a lock. This is the subtle one. Two overlapping runs can both read an issue as “unclaimed” and both start on it, because reading a label and then setting it isn’t atomic. A claim label helps as a fast guard, but the real fix is boring: never let two runs of the same agent overlap in the first place. The durable-state model gives you a lot, but it doesn’t give you mutual exclusion for free.
Loops don’t always converge. Sometimes the implementer and reviewer disagree in a way that doesn’t resolve — the same finding comes back round after round. Without a brake, that’s an infinite loop burning tokens all night. So there’s a retry budget: after a few failed rounds, the issue is pulled out of the automated queue entirely and labeled for a human. Knowing when to give up and ask for help is a feature, not an admission of defeat.
The takeaway
The instinct, when you want agents to collaborate, is to build something clever to coordinate them. I think that instinct is usually wrong. The clever part of this system is that there’s almost no coordination code at all. The agents are simple and replaceable. The intelligence lives in three ordinary places: a small set of legal states, a shared board that survives crashes, and a strict separation between doing work and blessing it.
Give the work a durable place to live, give each worker one job and no more authority than that job needs, and make someone who didn’t do the work check it before it counts. That’s not a recipe for agents. It’s how you’d design any team you actually trusted — which is, I think, exactly the point.