Grill, Slice, Ship: How I Work With Agents
On an average day I merge somewhere between 20 and 60 pull requests now (on my busiest day so far, GitHub counted 100 contributions), and I write almost none of the code in those PRs myself. The surprising part for me isn’t the PR count. The surprising part is that the slow part of my day moved: implementation is fast, and nearly all of my time goes into planning before an agent is allowed to touch the codebase.
In case that sounds made up, here’s my GitHub contribution graph for the last year. The dark squares on the right are the busy days, and the tooltip is sitting on the busiest one: 100 contributions on October 6th.
This post walks through the workflow I ended up with. The setup is a handful of small skills (a skill is just a prompt the agent loads for one specific job) chained together, plus one main Claude Code session that orchestrates everything. To be clear, I’m not claiming this is the correct way to work with agents. This is the way that works for me right now, and I fully expect to keep tweaking it.
Dump, Then Get Grilled
The process starts messy on purpose. I open a session and throw everything I have at the agent: what I want, why I want it, half-remembered links, decisions I’ve already made, things I’m unsure about, etc. I don’t try to write a specification at this stage because I don’t know the specification yet.
Then I ask the agent to grill me. My grill-me skill1 treats the plan as a tree of decisions, where each answer opens up the questions that depend on it. The interview happens in rounds. Each round contains all the questions the agent can ask without guessing at my other answers, with a recommended answer attached to each question. I answer, the agent recomputes the tree, and we repeat until no decision is left open.
The thing I do most in this phase is cut scope. I push toward the minimum in each round: what’s the smallest version of this application that’s still worth having? Which feature can wait for later? Agents are very happy to build every feature you mention, so if I don’t cut the scope here, nobody will.
This is the phase that takes the longest, and it should. The agent is good at finding gaps (an edge case I didn’t consider, two decisions that contradict each other, etc.), but deciding what to do about those gaps is my responsibility, and I can’t delegate that part. If I rush the grilling, I pay for it later in PRs that build the wrong thing very quickly.
A while ago I read a post about coding with AI that stuck with me. The gist: with AI it’s easy to get things right, but it’s just as easy to get them wrong, and a wrong decision doesn’t stay small. Everything downstream gets built on top of it, so by the time you notice, going back to the point where the decision was made costs a lot. And agents build on a bad decision just as fast as on a good one. That’s why I want the grilling and planning phase to be as thorough as I can make it. For a big project, the grilling alone sometimes takes days, or even weeks.
Slicing It Vertically
After the grilling, another skill, to-tickets, cuts the whole discussion into vertical slices.2
The common way to split work is by layer: one task for the database, one for the backend, one for the frontend, one for tests. That looks tidy, but each layer is waiting on the layer below. The frontend can’t start until the backend exists, and you can’t really test the backend until something calls the endpoints. So you end up with a chain of dependent tasks, and a chain doesn’t parallelize.
A vertical slice goes through all of the layers for one narrow piece of behavior. For example, say I’m building a bookmarks app. Instead of “all the tables, then all the endpoints, then all the pages”, the slices look like “a user can save a bookmark” (the table, the endpoint, the form and a test), then “a user can tag a bookmark”, then “a user can search their bookmarks”. The backend and frontend for a slice ship together because they’re one flow, so each slice is testable by itself, and most slices don’t block each other. If a slice really does depend on another slice, the dependency is written down explicitly instead of being hidden in the order of a list.
I create the repository first, then the slices get posted as GitHub issues. The idea turns into one parent issue, with each slice as a sub-issue under that parent and acceptance criteria and blockers linked. Any fully specified issue gets a ready-for-agent label, and that label is the gate: nothing gets implemented without the label.
I want to stress this part, because it’s the one I’d keep even if I threw out everything else: the GitHub issue is the record of every decision. What the grilling settled, the brief, the acceptance criteria, which run claimed it, the PR that implemented it, the follow-up somebody found during review, etc. It all lands on the issue body or in its comment history. So I don’t keep decision state anywhere else. No design docs folder, no DECISIONS.md, no notes file the agent has to be told about. An agent that picks up an issue (or comes back to it three PRs later) just reads the issue and has the whole story, and it can iterate from there.
The issues are the plan, and they’re also the memory. I’m not the first person to lean on GitHub issues this way, either. Simon Willison wrote about using them as his notebook in GitHub issues for notes: Markdown, links between issues across repos, search, and an API, all for free. It turns out those are exactly the features an agent needs too.
PRs hold context too (a bug the agent tripped over while building, a spot where it had to deviate from the brief, a review finding), but a PR is one attempt, and once it merges nobody goes back to it. One issue can also end up with more than one PR, so decisions spread across them would be hard to find. So the rule is simple: anything a PR finds that future work needs goes back to an issue. A changed decision goes into the original issue, and a new bug gets its own issue. Everything else on the PR is just evidence for the review.
Building and Merging
The implement-issues skill looks for ready issues, picks a batch that’s safe to run together (up to three at a time), and claims those issues so two runs never grab the same work. Each issue gets built in a separate git worktree on a separate branch, so the agents don’t step on each other’s files, and the batch goes up as a PR. If I point the skill at a parent issue, the agent works through the children in waves ordered by their blockers. Each wave is merged before the next wave starts, so later slices build on what’s actually in main.
Merging is a separate skill too. merge-pr reviews the open PRs, runs the checks, merges the safe ones and cleans up the branches and worktrees afterward. If a PR needs a follow-up, the merge agent routes the follow-up back instead of quietly merging.
I only talk to the main session. The main session hands the work out to the other agents and brings the results back to me.
The Harness Does the Heavy Lifting
One might ask: how do 60 PRs a day not turn into 60 PRs of garbage? The answer is the harness, and I want to stress this one, because without it none of the above would work.
Tests are part of the harness, but only part. Every change also has to pass my code quality checks, such as linting, formatting and type checking, with zero errors before the agent is allowed to call the work done. And there’s a second layer that tells the agent how to build things: skills for the frameworks I use (FastAPI, HTMX, Alpine.js, Polars, etc.) that spell out my conventions, so the agent follows the way I build instead of making up its own way in each PR. The agents are fast, but the harness is what lets me trust that fast output enough to merge it.
Keep It Looping
One batch of three is nice, but I don’t want to come back and type “implement the next ones” after each merge. So the main session runs implement-issues inside /loop:
/loop /implement-issues
The /loop command just runs a prompt repeatedly. You can give it a fixed interval such as /loop 10m ..., or leave the interval off and let the agent decide how long to wait. On each run, the agent reads the open issues, finds the ready-for-agent issues with no open blockers, and implements the next safe batch. After a batch merges, the slices waiting on that batch are unblocked and the next run picks them up. If nothing is ready, the loop waits and checks again later.
Small ideas skip most of this process. Halfway through the day I’ll think of something like a button in the wrong place, and I just tell the main session. The triage skill decides whether the idea needs grilling or whether the change is small and clear enough to post as an issue right away. If the issue goes straight in, the next run of the loop builds the change, opens a PR and merges it without any further input from me.
Sometimes I’m not even at my desk when an idea shows up. In that case I grab my phone, open the GitHub app, and create an issue with just a title and a very short description. That’s it. Later I revisit those issues with triage, which fills in the brief, grills me if the idea needs it, and only then adds the ready-for-agent label. Until that happens the loop ignores the issue, so a half-baked idea from my phone never gets built by accident.
So my job is to keep the issue list full of well-grilled issues, and the loop’s job is to keep emptying the list. That’s the whole setup for now.
-
grill-mecomes from Matt Pocock’s skills.↩︎ -
to-ticketsandtriagestarted as adaptations of Matt Pocock’s skills too, and the vertical-slice idea comes straight from there. I didn’t come up with it.↩︎