Skip To Content
← All posts

Building the /implement-with-agent-team skill

For most of this year, my Wednesday afternoons looked the same: open a ticket, run /implement, the skill that builds one ticket end to end, and watch an agent build it in a fresh context. What I noticed, ticket after ticket, wasn't really the code. It was my own behavior around it. I'd skim the diff, check that the tests it wrote actually failed before the fix and passed after, and merge. I'd stopped reading every line months before I admitted that to myself.

That wasn't recklessness. By the time a ticket ever reached /implement, it had already been through three rounds of scrutiny. I'd been grilled about it one question at a time until nothing was still a guess. That got folded into a spec. The spec got cut into a slice sized to fit one context and worth merging on its own. What the agent received wasn't a request so much as a decision I'd already made and written down. /implement just had to go execute it, test-first, against a checklist it wrote for itself and re-checked before ever calling the work done. Once I saw that clearly, the question stopped being "can I trust this output" and became "why am I still doing this one ticket at a time."

An epic is rarely one ticket. It's a dozen, with real edges between them: three that can start today, four that can't start until the first three close, one at the bottom of the graph that everything else quietly depends on. Running that manually meant becoming, personally, the scheduler: watching what had merged, figuring out what it unblocked, typing /implement again. /implement-with-agent-team is what happens when you take that job away from the person holding it, because the person was never the part doing anything hard. The skill calls me the lead: I dispatch, verify, and merge. It calls each agent it spawns a teammate: the teammate only writes code.

The problem with a shared to-do list

Before this skill, "run a few tickets in parallel" meant writing a prompt myself every time: here are the tickets, here's roughly what depends on what, go. I did that for over a month before I ever formalized it. It worked, but it was bespoke labor for every batch, and it depended on me keeping the dependency graph straight, from memory alone.

The tempting fix is a shared to-do list: some file sitting alongside the repo that every teammate reads from and writes back to, tracking which tickets are done, in flight, or blocked. It's also the wrong fix. That file would sit next to two records that already exist and already have to agree with each other: the git branches, and the issue tracker itself. A third record of the same facts doesn't remove that disagreement, it adds a third way for it to happen, and every drift is its own small debugging session. That's exactly the kind of state-sync bug that's tedious to find and depressingly easy to reintroduce the next time you change the code.

GitHub is already the state machine

The real fix is structural, not a smarter sync algorithm. GitHub already is the state machine:

  • The dependency graph already exists, as the tickets' native "blocked by" links.
  • Progress already exists, as open versus closed.
  • Completion already exists, because a merged pull request that references a ticket closes it.

So there's no second copy. A small script the skill carries with it, tickets.mjs, reads all of this fresh from the tracker on every call and writes exactly one thing back: a plain, readable comment on the ticket itself, the only claim mechanism it has. Something a person skimming the ticket later understands right away, not a code only the script can decode. A missed notification or a stale read stops being a bug, because the next call just re-derives the truth from the tracker instead of trusting something cached from a moment ago.

What you actually type

I invoke the skill myself; it never invokes itself mid-task. I tell it, in plain language, what to work on, an epic or a specific set of tickets, and it figures out the rest:

/implement-with-agent-team #1125
/implement-with-agent-team backend tickets of #1125
/implement-with-agent-team tickets 948 949 950

That second form matters more than it looks. When an epic spans two repositories, saying "backend tickets" is enough to scope one run to the backend half and leave the frontend half for a separate session, instead of one run trying to do both at once.

Reading the frontier

The skill's central idea is the frontier: every open ticket whose blockers have all closed. That's the set of work that can start right now, and tickets.mjs frontier recomputes it from the tracker on every call. Nothing about it is stored.

#948 DONE #949 READY #950 BLOCKED #951 BLOCKED
The frontier is every open ticket whose blockers have all closed. When #949 merges, #950 and #951 both become takeable at once, and both get dispatched together, in a single message.

Each ticket carries exactly one status, and only three of them expect anything from me:

Status Meaning My move
READY Blockers all closed, unclaimed Claim it and dispatch a teammate
BUILDING A teammate already has it Wait
REVIEW A teammate reported back, unverified Verify it, then merge
BLOCKED A blocker is still open Nothing; it frees itself
STUCK Nothing available can move it forward Report it, carry on without it
DONE Closed Nothing

The loop

I run this until the frontier reports the epic closed. It's genuinely cyclical: every merge can unlock new tickets, which is why a run keeps going instead of stopping after one pass.

  1. Read the frontier. The first action each time, including when resuming yesterday's run.
  2. Claim every ready ticket, up to seven at once. Claiming a ticket records the model it'll build with, sonnet by default, and the branch it should build against, and is what stops the next read from giving the same ticket to two teammates.
  3. Dispatch them all in one message, one fresh teammate per ticket, each in its own worktree, an isolated copy of the repo it can build in without affecting anyone else's. This is where the actual parallelism happens; spawning one at a time and waiting between each would make the whole run serial for no reason.
  4. Take each return: verify the pull request actually does what it claims, record it, merge it, confirm the merge actually closed the ticket rather than trusting the report that it would.
  5. Back to the top.

A teammate's brief is deliberately short. It names the ticket as owner/repo#number, so the teammate reads the real thing instead of my paraphrase of it. It says to invoke /implement and follow it, and it gives the branch and base I just claimed. It requires the pull request to say Closes #<number> in the body, and to come back with the PR number, what ran and its counts, and the teammate's own code-review output verbatim. If a rule in the ticket turns out to be wrong or impossible, the brief asks it to say so with evidence, not quietly work around it. I spawn every ticket in the batch this way, including the one that looks too small to bother with and the one in the repository I'm already sitting in. A ticket that looks too hard to delegate gets a missing fact added to its brief, not a decision I make myself instead.

By default a run doesn't target the org's actual default branch at all. The first ticket that needs one creates an integration branch named after the epic, and every ticket in the run builds against it. Merging a ticket's pull request onto that branch needs no approval from me, because it isn't shared history, nobody depends on it yet, and a bad merge there costs nothing but a follow-up ticket. The one pull request that does get reviewed by a human is the single one opened from that integration branch into the org's real default branch once the whole epic is done, and I open it, I never merge it myself. That's the one review that still happens manually, because it's the only merge in the whole run that actually changes the branch other people's work depends on.

When a ticket stops

When a ticket comes back without a working pull request, there are only two honest outcomes, and just one of them needs me. Either the teammate found a real dependency the graph was missing, in which case it gets recorded as a native "blocked by" link and frees itself automatically once that blocker closes, or nothing available can move the ticket forward, in which case the run reports it and continues without it rather than stalling the whole epic over one ticket.

Why a missed notification doesn't stop the run

Agent notifications occasionally just don't arrive. Because nothing about this skill is remembered between turns, a missed notification can't corrupt anything; the next frontier read just sees the pull request and continues. The real risk is quieter: the run can keep waiting, unaware the news already happened. So I keep a change feed running alongside the work, watching the epic and printing one line per status change, and it exits on its own once the epic is done. That still leaves one failure mode neither a notification nor a status change would ever reveal: a teammate that stopped before ever opening a pull request. There's no event for that, so the feed doesn't wait for one. A ticket sitting BUILDING for twenty-five minutes with no result yet gets flagged stale on its own, told apart from a build that's merely slow, and given to a fresh teammate instead of being abandoned.

Keeping the cost visible

Every teammate spawn has to name the model it's running on. That's a deliberate, enforced choice: the parameter is optional and inherits from whatever's running the skill, so an expensive model could easily work through a ticket sized for a cheap one, quietly, with nothing in the run's output ever saying so. A PreToolUse hook, code that runs right before a tool call and can block it, is set up automatically when the skill is installed. It refuses any spawn under this skill that hasn't stated its model explicitly, and it separately refuses one spawned as a fork, a lightweight spawn mode that always inherits the lead's own model regardless of what's asked for, so it can never actually be made cheap. The hook only looks at spawns this skill makes, so it never affects anything else running on the same machine. Most tickets are specced tightly enough that sonnet, the cheaper model, builds them correctly; the ones that need real judgment the spec can't pre-make get opus instead, deliberately, and the split gets reported at the end of the run.

What it deliberately doesn't do

This skill has a short list of jobs it deliberately refuses, and the refusals are as much the design as anything it does do:

  • It doesn't plan. Tickets arrive already specced, with blocking links already in place. A ticket set with no dependency edges at all gets returned rather than ordered by guesswork.
  • It doesn't write the brief. A teammate reads the ticket itself; the ticket is the spec.
  • It doesn't triage for me. Judging whether a review finding actually matters is my job, not something this skill decides for me.
  • It doesn't keep state. That's the one that made the rest of this possible: not tracking anything of its own is what lets the whole skill reduce to something small enough to actually reason about.

Everything that happens before it is where the actual trust gets built, one step at a time, long before any teammate writes any code.

When I have an idea worth turning into tickets, I start by getting grilled about it, with the grilling skill. That means one question at a time, never a batch, each one checked against a running tree of decisions, until nothing about the idea is still an assumption I never stated explicitly. If a question needs a fact rather than a choice, say, whether a piece of code already does something, or a table already has a column, a sub-agent checks it. I'm never asked to search for anything I don't have to. Only the real decisions come to me, one at a time, with a recommendation attached.

Once that conversation is done, to-spec synthesizes it, with no second interview, straight into a spec: a problem statement, user stories, the implementation and testing decisions we actually made, and what's explicitly out of scope. to-tickets then cuts that spec into tickets. Each one is a vertical slice through every layer the feature affects, sized to fit one context, and each one states exactly which other tickets have to finish before it can start. I get quizzed back on that breakdown before any of it ships, because getting the blocking edges wrong here is the one mistake that would break everything /implement-with-agent-team does afterward.

By the time a ticket reaches a teammate, the teammate invokes /implement to actually build it, and that adds one more layer of self-checking. Before writing any code, /implement turns every acceptance criterion into a gate in its own ledger, stated as something it can observe directly. It isn't allowed to call the work done while any gate is still unmet. It builds test-first at the seams the spec already agreed on, and it ends by running its own code review and committing.

Five things happen before an epic turns into code, and only the last one actually writes any of it. grilling interrogates the idea. to-spec writes the spec. to-tickets cuts the tickets. implement-with-agent-team runs the resulting graph to completion, one merge at a time. implement, running inside every teammate it spawns, is the one that writes the code. That order is what let me stop rereading every diff by the time it reached me. The ticket a teammate gets was never really a request. It was a decision I'd already made, several skills earlier, deliberately.

For a single ticket with nothing depending on it, I still just run /implement directly and skip all of the steps above. This skill is worth using specifically when there's a batch with real dependency structure between the pieces.

How to get it

It ships as a Claude Code plugin, in my fork of Matt Pocock's skill repo, bundled alongside to-spec, to-tickets, implement, grilling, and the rest of the set it depends on:

claude plugin marketplace add Odame/skills
claude plugin install odame-skills@odame

That fork-specific install matters: implement-with-agent-team is my own addition and only exists in Odame/skills, so Matt's own marketplace listing (claude plugin install mattpocock-skills, no @odame) won't have it. The plugin id is odame-skills, but every skill and slash command inside it still keeps the name Matt documents in his own repo; only the id differs. Everything else in the bundle is his, tracked against mattpocock/skills with any local changes recorded in PATCHES.md.