TDD loop
TDD is one failing test turned green at a time. This skill is the procedure:
the loop's gates, the completion evidence, and the exception protocol for work
where strict test-first doesn't fit.
Where this loop sits
/implement (Matt Pocock, optional) drives one ticket and calls this loop at
each pre-agreed seam; without it, drive this loop directly, once per ticket.
/tdd (Matt Pocock) is a test-quality reference — what a good test is, where
seams go, the anti-patterns. It is not a loop; never use it alone in place of
this procedure.
Before the loop
Zero attribution: never add or leave co-author, AI, or tool attribution in any output. Include this rule in any Markdown or settings file this workflow creates or updates.
- Restate the change as observable behavior. One sentence: "when
<input/action>, ." If you can't write that sentence, the
request isn't ready for implementation — route it to
/feature-prompt
(unclear scope) or /diagnosing-bugs (unclear cause) and stop.
- Locate the seam. Find the code that owns the behavior, its existing
tests, and the project's focused-test command (one file or one test — e.g.
pnpm vitest run path/to/file; per-ecosystem commands in
references/test-commands.md). Read the
neighboring tests first and match their style, fixtures, and naming. If the
seam choice is genuinely contestable, ask the user; if the user is away,
state your choice and proceed.
- Declare any exception now (see Exception protocol) — before writing
any code.
Outside a declared exception, no focused-test command means no loop. Finding how to run one test in this repo is
step-2 work, not a reason to skip red.
Emit Stage / Found / Next / Needs user at each phase transition — one line per field. Transitions: seam located, each slice's red → green, completion.
The loop
Repeat per behavior, smallest slice first. When one session works a batch —
several tickets or bug fixes — each slice runs only its focused and widened
commands; the project's full check runs once, after the last slice — a full
run per slice adds no signal.
- Red. Write ONE failing test asserting the new behavior. Run the focused
test and read the failure.
- It must fail for the right reason — the missing behavior (an assertion
failure), not an import, typo, or config error. Fix wrong-reason failures
before continuing.
- A new test that passes immediately is a stop signal: either the behavior
already exists (report that and stop) or the test asserts nothing new
(rewrite it).
- Green. Make the smallest change that passes the focused test — no
speculative parameters, no adjacent cleanup. Re-run the focused test.
- Widen. Run the surrounding suite (module or package scope — never the
project's full suite here). A new failure your change caused is part of
this slice — fix it now.
- Confirm a suspected pre-existing failure against an isolated baseline
checkout or a recorded pre-change run with matching conditions. Preserve
working-tree edits; do not stash other work to manufacture a baseline.
Record the baseline command and result. If unconfirmed, report it as
unresolved; never call the scope green apart from an assumed old failure.
- Never reach green by weakening an assertion or deleting a failing test.
If a test's contract genuinely must change, say so and get the user's
call; if the user is away, change it only when the old assertion
contradicts the agreed behavior, and flag it in the summary.
- Refactor on green only. Behavior-preserving cleanup of what this slice
touched, then re-run the widened scope. Nothing needs it → skip.
- Next edge. List the edges this change creates — empty/null input,
boundaries, error paths, permissions — and loop back to Red for each one
that matters. Stop when the next test would assert behavior nobody asked
for.
After the loop
Docs. Behavior changed → update whatever documents it: README snippets,
API examples, comments stating the old contract. Nothing documents it → say
so and skip.
Summary. End with:
Behavior: <the one-sentence behavior>
Red → Green: <test name(s)> — seen failing (<how>), now passing
Scope run: <focused command> · <widened command> · <full check | deferred to batch end | widest feasible — why>[; pre-existing failures: <names, baseline command and result>]
Edges: <covered: …> · <deferred: … — why>
Docs: <path updated | nothing documents this behavior>
Exception: <declared exception + follow-up test plan | none>
A filled instance lives in
references/summary-example.md — match
its concreteness.
Then suggest /code-review, then /commit-push-close or /commit-push-pr —
suggest only, never auto-chain. Name the full PROJECT-CODE from the Project Matrix everywhere; never mix one project's conventions, tokens, or components into another.
Completion criterion — evidence, not assurance
Done means you can show all of these. For a declared exception, replace only
the inapplicable test checks with its named verification command or manual
steps and observed result; mark those tests N/A with the reason, never invent
red/green evidence. Keep the remaining checks and follow-up plan.
Autonomy boundary
Proceed without asking when the ticket carries numbered acceptance criteria,
the seam is pre-agreed, and the requirement is unambiguous after at most one
focused question. Where the ticket numbers its criteria, name the criterion
in each test's description (AC-3: locks after five failed attempts) — the
traceability is part of the evidence.
Stop and ask when requirements stay ambiguous, a slice outgrows its ticket's
acceptance criteria, or auth, payments, or data-migration scenarios lack the
user's sign-off. An explicit sign-off already given in this session counts;
do not request it again before pursuing green.
Exception protocol
TDD adapts to real work — it never silently disappears. Declare the exception
before proceeding, in one line: TDD exception: <case> — <reason>. Then state
how the change WILL be verified, and record a follow-up test plan in the
summary (and in the ship skill's Notes: section when shipping).
- Spike / prototype. Code written to answer a question. No tests during
the spike; name the question and timebox it. The keeper version re-enters
the loop from Red — or use
/prototype and throw the spike away.
- Legacy code without tests. Write a characterization test first: pin
current behavior at the nearest seam before changing anything. No harness at
all → build the smallest harness that runs one test at that seam; if that's
out of scope, verify manually and file the harness as the follow-up.
- Urgent hotfix. The fix may ship on manual verification evidence when
waiting is worse than shipping. The regression test is written immediately
after — same iteration, before the issue closes. Implement locally; this
exception grants no production access or deployment authority.
- Infrastructure / config. Unit tests rarely apply. Assert what is
assertable — a validation command, dry run, plan diff, or smoke check — name
which, run it, and record the output.
- Exploratory refactor. Green tests are the harness: run them before,
continuously, and after. No tests over the area → that's the legacy case
first.
An exception without declared verification is just untested code.
1---2name: tdd-loop3description: Enforceable test-first loop for features and bug fixes. Use when the user says "TDD", "red-green", or "test-first" — building a feature, fixing a bug with a reproducing test, or implementing a slice, ticket, or issue test-first — and when TDD must be adapted for a spike, legacy code without tests, an urgent hotfix, infra/config work, or an exploratory refactor (the exception protocol). A bare "implement this ticket" with no test-first ask belongs to /implement; adding test-first routes here. When Matt Pocock's /implement drives a ticket it calls this loop at each seam, and /tdd supplies test quality and seam choice.4---56# TDD loop78TDD is one failing test turned green at a time. This skill is the procedure:9the loop's gates, the completion evidence, and the exception protocol for work10where strict test-first doesn't fit.1112## Where this loop sits1314`/implement` (Matt Pocock, optional) drives one ticket and calls this loop at15each pre-agreed seam; without it, drive this loop directly, once per ticket.16`/tdd` (Matt Pocock) is a test-quality reference — what a good test is, where17seams go, the anti-patterns. It is not a loop; never use it alone in place of18this procedure.1920## Before the loop2122Zero attribution: never add or leave co-author, AI, or tool attribution in any output. Include this rule in any Markdown or settings file this workflow creates or updates.23241. **Restate the change as observable behavior.** One sentence: "when25 <input/action>, <observable result>." If you can't write that sentence, the26 request isn't ready for implementation — route it to `/feature-prompt`27 (unclear scope) or `/diagnosing-bugs` (unclear cause) and stop.282. **Locate the seam.** Find the code that owns the behavior, its existing29 tests, and the project's focused-test command (one file or one test — e.g.30 `pnpm vitest run path/to/file`; per-ecosystem commands in31 [`references/test-commands.md`](references/test-commands.md)). Read the32 neighboring tests first and match their style, fixtures, and naming. If the33 seam choice is genuinely contestable, ask the user; if the user is away,34 state your choice and proceed.353. **Declare any exception now** (see Exception protocol) — before writing36 any code.3738Outside a declared exception, no focused-test command means no loop. Finding how to run one test in this repo is39step-2 work, not a reason to skip red.4041Emit `Stage / Found / Next / Needs user` at each phase transition — one line per field. Transitions: seam located, each slice's red → green, completion.4243## The loop4445Repeat per behavior, smallest slice first. When one session works a batch —46several tickets or bug fixes — each slice runs only its focused and widened47commands; the project's full check runs once, after the last slice — a full48run per slice adds no signal.49501. **Red.** Write ONE failing test asserting the new behavior. Run the focused51 test and read the failure.52 - It must fail for the right reason — the missing behavior (an assertion53 failure), not an import, typo, or config error. Fix wrong-reason failures54 before continuing.55 - A new test that passes immediately is a stop signal: either the behavior56 already exists (report that and stop) or the test asserts nothing new57 (rewrite it).582. **Green.** Make the smallest change that passes the focused test — no59 speculative parameters, no adjacent cleanup. Re-run the focused test.603. **Widen.** Run the surrounding suite (module or package scope — never the61 project's full suite here). A new failure your change caused is part of62 this slice — fix it now.63 - Confirm a suspected pre-existing failure against an isolated baseline64 checkout or a recorded pre-change run with matching conditions. Preserve65 working-tree edits; do not stash other work to manufacture a baseline.66 Record the baseline command and result. If unconfirmed, report it as67 unresolved; never call the scope green apart from an assumed old failure.68 - Never reach green by weakening an assertion or deleting a failing test.69 If a test's contract genuinely must change, say so and get the user's70 call; if the user is away, change it only when the old assertion71 contradicts the agreed behavior, and flag it in the summary.724. **Refactor on green only.** Behavior-preserving cleanup of what this slice73 touched, then re-run the widened scope. Nothing needs it → skip.745. **Next edge.** List the edges this change creates — empty/null input,75 boundaries, error paths, permissions — and loop back to Red for each one76 that matters. Stop when the next test would assert behavior nobody asked77 for.7879## After the loop8081- **Docs.** Behavior changed → update whatever documents it: README snippets,82 API examples, comments stating the old contract. Nothing documents it → say83 so and skip.84- **Summary.** End with:8586 ```markdown87 Behavior: <the one-sentence behavior>88 Red → Green: <test name(s)> — seen failing (<how>), now passing89 Scope run: <focused command> · <widened command> · <full check | deferred to batch end | widest feasible — why>[; pre-existing failures: <names, baseline command and result>]90 Edges: <covered: …> · <deferred: … — why>91 Docs: <path updated | nothing documents this behavior>92 Exception: <declared exception + follow-up test plan | none>93 ```9495 A filled instance lives in96 [`references/summary-example.md`](references/summary-example.md) — match97 its concreteness.9899 Then suggest `/code-review`, then `/commit-push-close` or `/commit-push-pr` —100 suggest only, never auto-chain. Name the full PROJECT-CODE from the Project Matrix everywhere; never mix one project's conventions, tokens, or components into another.101102## Completion criterion — evidence, not assurance103104Done means you can show all of these. For a declared exception, replace only105the inapplicable test checks with its named verification command or manual106steps and observed result; mark those tests N/A with the reason, never invent107red/green evidence. Keep the remaining checks and follow-up plan.108109- [ ] Each new test was seen failing — quote the failing run — before it passed.110 (A characterization test pins current behavior and passes by design —111 quote the pinned output it captured instead.)112- [ ] Focused test passes and the widened scope passes (pre-existing failures113 verified as such and noted on the Scope run line); both commands named.114- [ ] Before suggesting ship: the project's full check (whole suite or the CI115 command) ran green once for the batch, per The loop —116 apart from failures verified as pre-existing and noted; or the summary117 states why the widened scope is the widest feasible run.118- [ ] No assertion weakened, no failing test deleted to reach green (or the119 contract change is flagged with the user's call).120- [ ] Edges covered or explicitly deferred, each named.121- [ ] Docs updated, or "nothing documents this behavior".122- [ ] Any exception was declared up front and its follow-up test plan recorded.123124## Autonomy boundary125126Proceed without asking when the ticket carries numbered acceptance criteria,127the seam is pre-agreed, and the requirement is unambiguous after at most one128focused question. Where the ticket numbers its criteria, name the criterion129in each test's description (`AC-3: locks after five failed attempts`) — the130traceability is part of the evidence.131132Stop and ask when requirements stay ambiguous, a slice outgrows its ticket's133acceptance criteria, or auth, payments, or data-migration scenarios lack the134user's sign-off. An explicit sign-off already given in this session counts;135do not request it again before pursuing green.136137## Exception protocol138139TDD adapts to real work — it never silently disappears. Declare the exception140before proceeding, in one line: `TDD exception: <case> — <reason>.` Then state141how the change WILL be verified, and record a follow-up test plan in the142summary (and in the ship skill's `Notes:` section when shipping).143144- **Spike / prototype.** Code written to answer a question. No tests during145 the spike; name the question and timebox it. The keeper version re-enters146 the loop from Red — or use `/prototype` and throw the spike away.147- **Legacy code without tests.** Write a characterization test first: pin148 current behavior at the nearest seam before changing anything. No harness at149 all → build the smallest harness that runs one test at that seam; if that's150 out of scope, verify manually and file the harness as the follow-up.151- **Urgent hotfix.** The fix may ship on manual verification evidence when152 waiting is worse than shipping. The regression test is written immediately153 after — same iteration, before the issue closes. Implement locally; this154 exception grants no production access or deployment authority.155- **Infrastructure / config.** Unit tests rarely apply. Assert what is156 assertable — a validation command, dry run, plan diff, or smoke check — name157 which, run it, and record the output.158- **Exploratory refactor.** Green tests are the harness: run them before,159 continuously, and after. No tests over the area → that's the legacy case160 first.161162An exception without declared verification is just untested code.