implement-tickets
Run the implement-ticket skill's process once per ticket, sequentially, each in a fresh context: implement, verify a commit landed, review, repair, move on.
Skills this composes with
All user-level, at ~/.claude/skills/<name>/SKILL.md:
- to-tickets — produces the tickets this consumes. Not invoked here.
- implement-ticket — the per-ticket process. The worker subagent reads this file and
follows it. It does not invoke it: a subagent cannot reliably trigger a skill, and reading
the file works regardless. Note this is
implement-ticket, singular — a separate skill from the general-purposeimplement, which this run never touches. - code-review — invoked by you, the parent, once per ticket. Never by the worker: it spawns two parallel subagents of its own, and a worker is already a subagent.
If ~/.claude/skills/implement-ticket/SKILL.md does not exist, say so and stop rather than
improvising a process — the point of this skill is sequencing that one, not replacing it.
Where the tickets are
Default .scratch/<feature-slug>/issues/<NN>-<slug>.md. If the user names a different
folder, use that. Tickets are already numbered in dependency order — blockers first — so
filename order is execution order and no graph reasoning is needed.
Each ticket carries: a title, What to build (end-to-end behaviour, not layers), Blocked by, Status, and acceptance criteria as checkboxes. A ticket is a tracer bullet: a narrow but complete path through every layer, demoable on its own.
Before starting
Working tree must be clean.
git status --porcelain— if it returns anything, say what is dirty and stop. One exception: if the only dirt is inside the ticket folder, it is bookkeeping left by an earlier run — commit it aschore: ticket statusand carry on.Fix
TICKET_ROOT— the feature root that holds the tickets, normally.scratch. Every cleanup in this run excludes that path, which is what makes the ticket set safe without needing to hide it from git. Do not require the user to gitignore it: gitignored paths vanish from@-mention and file search in Claude Code and Cursor, and the tickets are the one thing they most need to reference by hand mid-run.The folder may be tracked, ignored, or neither — all three work. Two failure modes are ruled out by the scoping rather than by the ignore rules:
- An unscoped
git clean -fddeletes an untracked, un-ignored ticket folder outright — the entire remaining set, at the exact moment a run fails.-e $TICKET_ROOTprevents it. - An unscoped
git reset --hardreverts every Status edit made so far when the folder is tracked, so the resume logic re-runs work that already landed. The':(exclude)'pathspec prevents that.
- An unscoped
One command to read the whole run's shape without opening any ticket:
grep -H -E "^\*\*Blocked by:|^\*\*Status:" <folder>/*.mdTwo short lines per ticket. Check that every blocker is a lower number than the ticket naming it. If any blocker sorts later, report which and stop — renumbering costs nothing now, and costs a wasted run later.Any ticket whose Status is already
doneis skipped. The first ticket that is not done is the frontier; start there. This is also how you resume after an interruption.If
<folder>/../CONTEXT.mddoes not exist, create it with the single line# Context log. This is the only channel between tickets.Record the starting commit:
BASE=$(git rev-parse HEAD). One line, and the optional cross-ticket pass at the end needs it.Announce the count and begin. Do not ask for confirmation.
Keep your own context small
You persist across every ticket; the workers do not. Do not read source files, diffs, test output, or ticket bodies — step 3 above is the only bulk read you do. Pass the ticket PATH; the worker reads the file. Do not summarize or reflect on finished tickets.
The per-ticket review is the one deliberate exception: it returns roughly a thousand tokens each time, which is affordable for a run of eight or ten tickets and is not affordable for twenty-five. Past roughly fifteen tickets, tell the user you are switching to a single end-of-run review instead, and do that rather than silently filling up.
For each ticket, in filename order
1. Dispatch
Announce one line: === <NN>-<slug>, then capture PREV=$(git rev-parse HEAD) and dispatch
a subagent (Task) with this prompt, substituting the paths:
Implement the ticket at <absolute-path-to-ticket>. That ticket only — no
opportunistic refactors, no drive-by fixes, no dependency bumps.
It is a tracer bullet: a narrow but COMPLETE path through every layer it touches
— schema, API, UI, tests. Deliver the whole path, not one layer of it. When you
are done the behaviour in "What to build" must actually work end to end.
Read <absolute-path-to-CONTEXT.md> first — earlier tickets recorded the interfaces,
decisions and constraints you need.
Then READ ~/.claude/skills/implement-ticket/SKILL.md and follow its process for this one
ticket. Read it as a file; do not try to invoke it. If it points at other skills
(/tdd and the like), read those the same way at ~/.claude/skills/<name>/SKILL.md.
TWO OVERRIDES, and they win over anything that file says: implement this ONE
ticket, not the wider spec or the rest of the set; and do NOT run code-review —
your work is reviewed after you exit, by agents that did not write it, and a
review you run on yourself in this context is worth much less.
Also follow, where they exist: CLAUDE.md, and any ADRs or domain glossary it
points to. Use the project's existing vocabulary for names. Conventions are
binding; do not re-litigate them.
TDD at the seams the ticket names. Typecheck and run single test files as you go.
Run the full suite once before committing and leave it GREEN — unless this ticket
explicitly says green is only promised at a later integrate-and-verify ticket, in
which case say so in your return.
Before committing, check your own diff against every acceptance criterion in the
ticket, line by line. This is a sanity check, not the review — your work is
reviewed afterwards by agents that did not write it.
Tick each acceptance criterion checkbox in the ticket file as you satisfy it. Do
NOT touch the ticket's Status line — that is set outside this context. Then append
at most 4 lines to <absolute-path-to-CONTEXT.md> under a `## <NN>-<slug>` heading:
INTERFACE: public surface added or changed
DECISION: a non-obvious choice AND why — "used Redis" is worthless,
"Redis SETNX over a DB constraint: sub-ms needed, orders table hot"
is not
CONSTRAINT: an invariant later tickets must not break
DEFERRED: what you left, and to which ticket
Omit any that do not apply. "## <NN>-<slug> — nothing notable" is a valid entry.
Do not pad to fill the four lines.
Commit exactly once to the current branch. The commit body must contain the line
`Ticket: <NN>-<slug>`. Never amend, rebase, push, or tag. Leave no source change
uncommitted. Bookkeeping edits inside the ticket folder that you did not make are
not yours to commit or revert — leave them exactly as you found them.
THERE IS NO HUMAN AVAILABLE TO YOU. If the acceptance criteria are ambiguous or
contradictory, if the ticket depends on code that does not exist in the repo, or
if it needs a product decision or credential you do not have — make NO commit and
return `BLOCKED <NN>-<slug> | <the specific question>`. Guessing costs more than
stopping does.
Your final message must be AT MOST 2 lines:
DONE <NN>-<slug> | <one clause the next ticket needs to know>
BLOCKED <NN>-<slug> | <the specific question>
No summaries, no file listings, no explanation of what you did.
2. Confirm the work exists
git rev-parse HEAD — if it still equals $PREV, the worker claimed DONE and committed
nothing. Treat that exactly as BLOCKED (below). This costs fifteen tokens and catches the
failure that would otherwise be silent: the next failure's cleanup would restore the
source tree over that uncommitted work, and nothing would say so.
On BLOCKED, or a worker error, or no commit → clean up with the two scoped commands,
never a bare git reset --hard:
git restore --source=HEAD --staged --worktree -- . ':(exclude)<TICKET_ROOT>'
git clean -fd -e <TICKET_ROOT>
Then report the question verbatim, leave that ticket's Status alone, and STOP. Do not attempt later tickets: they may depend on this one, and building on a gap produces work that gets thrown away rather than fixed.
3. Mark it done
Edit that ticket's **Status:** line to done. You are the only writer of Status. The
worker is forbidden from setting it precisely because a worker that dies between marking
itself done and committing would leave a ticket that looks finished and isn't — and the
resume logic would skip it forever.
Flip it here, after the commit is confirmed and before the review. If the user interrupts mid-review, resuming should not re-implement work that already landed; the end-of-run pass still covers anything the interrupted review would have caught.
4. Review it
Invoke the code-review skill with fixed point $PREV and the ticket file as the spec
source. Both axes run in their own subagents, so the code is reviewed by contexts that did
not write it. Print the two reports as they come back.
5. Act on the findings — and only these
Bounded on purpose. An unbounded polish loop is the failure mode here.
Hard violations (a documented standard breached) and Spec axis (a) (a requirement the ticket asked for that is missing or partial) → dispatch one fix-up worker:
The ticket at <absolute-path-to-ticket> was implemented and committed, then reviewed. Fix only the findings listed below — nothing else. Do not refactor beyond them, do not address anything not listed, do not touch other tickets' work. FINDINGS: <the hard violations and missing/partial requirements, verbatim> Run the full suite and leave it green. Commit exactly once, body containing `Ticket: <NN>-<slug> (review fixes)`. Leave nothing uncommitted. Return AT MOST 2 lines: FIXED <NN>-<slug> | <one clause> NOTHING TO FIX <NN>-<slug> | <why the findings need a human>One attempt only. Whatever comes back, print it and continue — a second fix-up round is how a run turns into an afternoon.
Judgement calls (the smell baseline) and Spec axis (b) (scope creep) → print, do not act. These need the user's eye. A worker acting unsupervised on "possible Feature Envy" produces churn, not improvement.
Spec axis (c) (a requirement that looks implemented but where the implementation looks wrong) → STOP the run and surface it. This is the finding that quietly poisons every downstream ticket, because later tickets build on the wrong behaviour and their own reviews come back clean.
Then move to the next ticket.
At the end
- If the ticket folder is tracked and has uncommitted Status or checkbox edits, commit
them once as
chore: ticket status. Do this when stopping early too — otherwise the next run's clean-tree check trips on your own bookkeeping. - Run the project's test suite once.
- Report three things and nothing else: how many tickets completed, the last commit sha, and whether the suite is green.
- Offer — once, in one line, never automatically — a Standards-axis code-review over
$BASE...HEAD. Per-ticket reviews structurally cannot see cross-ticket smells: Duplicated Code between ticket 02 and ticket 07, or Shotgun Surgery spread across the feature, where each ticket's diff is clean in isolation. This is the only pass that sees the feature as one change. - Offer — once, in one line — to promote anything durable out of CONTEXT.md into CLAUDE.md. Most of that log is scaffolding that should die with the feature; the occasional entry is a real project-wide convention or ADR-level decision that every future session should know. Promoting is the user's call, not yours, and never automatic.
If the tickets live on a real tracker
Same loop, different lookup. The frontier is the issues labelled ready-for-agent whose
blocking issues are all closed. Use the tracker's CLI or MCP tools to list and fetch; pass
the worker the issue URL instead of a file path, and pass code-review the same URL as its
spec source. Keep using CONTEXT.md for the handoff — re-fetching issues for context costs
far more than reading one local file. After a DONE, close the issue or drop the label,
whichever the project does; that replaces the Status edit, and it is still yours to do,
not the worker's.
If the developer interrupts
They may hit Esc and talk to you mid-run. Answer, then resume from the first ticket whose
Status is not done when they say continue. Determine that by re-running the grep from
step 3 of the pre-checks — not from memory.