Ticket flow
One ticket at a time. The human fires each stage by hand, in a clean session. The ticket is the contract; the commit is the handoff — no intermediate document. A trivial task goes straight in, no loop.
This file owns the build loop, and it is the only copy. Where tickets live and
how triage labels them come from the repo's docs/agents/issue-tracker.md, written
by /setup-matt-pocock-skills. That skill and the ones the stages call are
vendored: never edit them to fit this loop. They get reinstalled, and an edit
there disappears without a word. What the loop needs and they do not define is
defined here, in "What this loop adds" below — nowhere else.
Two things to read before acting
docs/agents/issue-tracker.md— where tickets live, and the triage vocabulary iftriageis in use. Missing? The repo was never set up: tell the user to run/setup-matt-pocock-skillsand stop.- The
## Bindings do fluxoblock inAGENTS.md(orCLAUDE.mdif there is noAGENTS.md) — gate command, base branch, model per stage. Missing? See the last section. Never guess the gate command.
Stages
| # | Skill | Delivers | Stops and reports if |
|---|---|---|---|
| 1 | grill-me → to-spec → to-tickets |
spec.md + one ticket file per ticket, Stage: to-implement |
the human does not approve the seams or the slicing |
| 2 | tdd |
branch named for the id, Stage: implementing; a test-only commit, red for the right reason; then code commits that do not touch tests; gate green; Stage: to-review |
the ticket needs more than one seam (back to stage 1), or a test proves wrong after being committed (Stage: blocked + reason) |
| 3 | code-review (Standards + Spec axes) |
small fixes; then either merge, or a PR that waits | a finding is large — reopen the ticket, back to stage 2 |
Stage 1 also registers new ids in the repo's plan document, if it has one, at the position of the original. That is the only edit anyone makes to the plan.
Refactoring outside what the ticket touched is not part of any stage. It becomes
its own CLEAN-* ticket.
Two overrides on the skills the stages call:
- Stage 2 does not re-negotiate seams. The
tddskill writes no test at a seam the user has not confirmed; here the ticket is that confirmation — its Primary files and its "Tests stage 2 writes" section name the seams, approved when the human approved the ticket. Stop only if the ticket needs a seam it does not name, which is the back-to-stage-1 case. - Nobody refactors mid-loop.
tddparks refactoring in the review stage; the review parks anything outside the ticket's Primary files in aCLEAN-*ticket.
Stage
Stage: to-implement | implementing | to-review | reviewing | to-merge | done | blocked
This is the loop position, and it is not the triage Status: line. Different
axes: Status: ready-for-agent with Stage: reviewing is a coherent ticket. Never
merge them into one field, and never read one as the other.
No stage change without a commit. The stage is what the next session dispatches on; a stage that moved in a dirty working tree is a stage that can lie.
| Transition | Who | In which commit |
|---|---|---|
→ implementing |
stage 2 | the test-only commit, its first |
→ to-review |
stage 2 | its last code commit, gate green |
→ reviewing |
stage 3 | only if it commits a fix of its own |
→ done |
stage 3 | the same commit as the ledger line |
→ to-merge |
stage 3 | the commit the PR is opened from (no-session flow only) |
→ to-implement |
stage 3 | the reopen commit |
→ blocked |
anyone | with the reason under ## Comments |
What this loop adds
to-tickets publishes a local ticket as # <NN>: title with a
**Status:** ready-for-agent line and checkbox criteria. That is a triage queue
entry, and it cannot carry this loop: no address to cite in a commit, no field to
dispatch on, no declared boundary. When stage 1 publishes to local files, write
this shape instead of that skill's template — and change nothing else about how
that skill works. Its vertical slicing, blocking edges, expand–contract sequencing
for wide refactors and the approval quiz are why it is being called.
# <AREA>-<NNN>: <Ticket title>
Stage: to-implement
Blocked by: <the ids that gate this one, or "none">
Review: agent | human
- Primary files:
- <path the ticket may touch> (<scope, when the whole file is not in play>)
- New: <path>
#### What to build
The end-to-end behaviour this ticket makes work, from the user's
perspective, not a layer-by-layer implementation list.
#### Acceptance criteria
1. <criterion, stated so a test can fail it>
2. <criterion>
#### Verification
<the commands that prove it, gate last>
## Tests stage 2 writes (own commit, red)
- <which file, at which seam, red for which reason before the change>
## Comments
Five additions, and each earns its place:
- The id.
<AREA>-<NNN>, unique across the whole repo and immutable — it is the address every ledger line, commit message and review block cites. The filename keeps the vendor's positional prefix as well (<NN>-<ID>-<slug>.md): the number is dependency order and may go stale, the id never does. Reuse a prefix already in use before inventing one; next number is the highest existing for that prefix anywhere in the tracker, plus one (grep -rhoE 'BUG-[0-9]+' <tracker> | sort -t- -k2 -n | tail -1). A ticket that turns out to belong to another area keeps its id and says so in its body. Stage:. Its own section, above. It is a second line, never folded into the vendor'sStatus:— different axes.Primary files.to-ticketssays to keep file paths out of a ticket because they go stale. True of prose; this list is not prose, it is the boundary — the implementer may touch those files and nothing else. Keep it to the files in play, never an implementation plan.- Numbered criteria, not checkboxes, so a review can fail "criterion 5" by name.
Review:. Who clicks merge.agent(the default when the line is missing) lets stage 3 merge an approved PR itself;humanholds the PR for the person. Stage 1 sets it when writing the ticket — the author decides what they want to see, not the reviewer. Inside a session branch (below) nothing is held:humanonly puts the ticket at the top of the session PR.
Also per effort directory: ledger.md, a | Data | ID | Commit | table of closed
tickets, one line each, written when a ticket reaches done.
Dispatch on a bare ID
A session handed nothing but an id (OBS-004) finds the ticket — the tracker file
says how, usually grep -rl '<ID>' <tracker path> — and dispatches on Stage.
Find the loop's base first. It is the bindings' base branch — unless a
sweatshop/* branch exists that is not merged into it (git branch --list 'sweatshop/*' --no-merged <base>, remote too). Then that is the base for
stages 2 and 3: branch from it, rebase onto it, merge into it. It is the
sweatshop driver's session branch, collecting every ticket of a run into one
PR for the human. With no such branch, nothing below changes.
Find the ticket's branch before reading Stage. The stage is committed on
the ticket's branch (stage 2 names it after the id, lowercased: obs-004). The
copy on the base is stale until merge, so a fresh session on master reads
to-implement on a ticket that is actually waiting for review. Run
git branch --list '<id lowercased>' first; if the branch exists and is not
merged into the loop's base, check it out and read the ticket there. Only then
dispatch:
| Stage | What you do |
|---|---|
to-implement |
Stage 2. Call the tdd skill. |
implementing |
A session stopped mid-run. Report what the branch holds and stop — do not continue blind. |
to-review |
Stage 3. Call the code-review skill. |
reviewing |
Same as implementing: report and stop. |
to-merge |
The PR waits on the human (no-session flow only). Report it and stop. |
done |
Nothing to do. Say so and stop. |
blocked |
Report the reason from ## Comments and stop. |
Three guards:
to-implementwith an unmerged branch is a stale read. You skipped the branch check above. Check the branch out and dispatch again.- Wrong model, no work. If the bindings assign that stage to a model you are not, say which stage the ticket wants and which model owns it, then stop.
- Reopened tickets look new.
Stage: to-implementon a ticket that carries a stage-3 review section means only the ❌ items are left, and the work continues on the existing branch. Read the ticket to the bottom before starting.
The rules that hold the loop up
Tests go in their own commit, red, before any code. The load-bearing check is
the diff --stat separation, not any agent's promise. Whoever writes the tests
does not make them pass in the same commit, and stage 2 does not touch test files
in a code commit. This rule exists because green tests have sat on top of a broken
parser — the tests were exercising a copy of the logic.
Primary files and the numbered criteria are the contract. Stage 2 reads those two lists and treats them as binding. A requirement stated only in prose is invisible: it gets met by accident or not at all.
Small fix, or back to stage 2 — decided mechanically. A fix is small if it fits inside the ticket's Primary files and needs no new test. If it needs a new test, or touches source outside the Primary files, stage 3 does not fix it: reopen, back to stage 2. No exceptions — a reviewer judging size by feel will always find its own findings small.
A finding that belongs to another ticket goes under ## Comments on that
ticket — never into its body. Only stage 1 moves a comment into the body, and
when it does it adds the file to Primary files and a numbered criterion. An
unfolded comment is a note, not a requirement: prose in a body that no file and no
criterion backs is invisible to stage 2, which will ship green without it.
Red-green proof before reporting. Show that the new tests fail without the change and pass with it. Report the real suite counts, not rounded ones. Every bug fix has a test that would fail without it, and that test calls production code, never a copy of it.
Reopening. A review that knocks down a closed ticket sets
Stage: to-implement, marks which criterion fell (❌ with the reason), removes the
ledger line, and appends a review block saying what is left.
Commits and closing
Conventional Commits in English, the body saying why and citing the id
(fix: ... (BUG-003)). Never squash.
Stage 3 closes by appending a #### Resolution (YYYY-MM-DD) block to the ticket —
decision, files, red-green proof, gate output — and adding the ledger line, in the
same commit as Stage: done. The ticket is the memory between sessions.
The review's verdict is one line, and it is the first line of the Resolution
block: Verdict: Approve, or Verdict: Needs your call: <one sentence why>.
The findings follow. "Needs your call" is for anything the reviewer is not
confident about, including a small fix it made itself that it would rather have
a human glance at. A wrong "Approve" costs more than a held ticket.
Merge depends on the loop's base:
Into a session branch (a sweatshop/* is open):
- Rebase the ticket branch onto the session, gate green.
- A conflict inside the ticket's Primary files: resolve it. Outside them:
Stage: blockedwith the conflict under## Comments, commit, stop. git checkout <session>; git merge --no-ff <ticket branch>, then the Resolution block, the ledger line andStage: donein one commit on the session. No PR, no push: the driver pushes and opens the session PR when the run stops, every verdict in its body.Review:holds nothing here.
Into the bindings' base (no session), always through a PR:
- Rebase the branch onto the base branch, gate green, push,
gh pr create. - Post the review as one PR comment, verdict first.
Review: agentand the verdict isApprove: wait for CI (gh pr checks --watch), thengh pr merge --merge --delete-branch, pull the base branch, setdonethere with the ledger line. Never squash.Review: human, or the verdict isNeeds your call: setto-merge, leave the PR open, stop.
Unattended runs
The sweatshop skill's driver feeds bare ids to claude -p, one at a time, and
reads Stage: back. It adds no instructions of its own: everything a session
does, it does because this file says so. Nobody is watching, so a session owes it
three things:
- A question is a
blocked. There is no one to answer "which do you want?". Stage 2 that needs a decision (a seam the ticket does not name, a blank the spec left) touches nothing, setsStage: blockedwith the question under## Comments, commits, and stops. Stage 3 that is unsure writesNeeds your calland finishes; it never ends atreviewingasking what to do. - One stage per session. Stage 2 stops the moment
to-reviewis committed; it does not review its own work. Stage 3 stops atdone,to-merge,to-implementorblocked, nothing else. - A failed attempt leaves a trace. The driver writes
Attempt N failed: <reason>under## Comments, committed on the branch. A session that finds one is a retry: read it first.
Answering is a stage-1 job, done once per batch: read the tree the driver prints
when it stops — every blocked node carries the question under it — then fold
each answer into the ticket body (Primary files, a criterion, a filled blank),
set to-implement, commit on the loop's base, run the driver again. Everything
else about the driver — the session branch, the run log, how to start it — is in
sweatshop/SKILL.md.
A repo with no bindings block
Ask for the three values, write the block where it belongs, then get on with the work. Do not copy this skill into the repo — one copy of the standard is the point.
## Bindings do fluxo (skill `ticket-flow`)
- Gate: `<command that runs typecheck + lint + tests>`
- Base branch: `<name>`
- Models: stage 1 <model>, stage 2 <model> [effort], stage 3 <model> [effort]
O [effort] é opcional (low|medium|high|max) e só o driver lê; sem ele, high.
Tracker paths and ticket shape do not go in this block. They are already in
docs/agents/issue-tracker.md.