/tdd-loop
Drive the red-green-refactor loop for any non-trivial code change. Auto-detect the project's test runner (pytest / vitest / jest / node --test), invoke it via the `tdd.py` helper, and act on the normalized exit code. Use when you write a test, TDD this feature, let me test-drive
Claude Code
$ npx -y skills add ramboz/jig --skill tdd-loop --agent claude-code
Codex
$ npx skills add ramboz/jig --skill tdd-loop -a codex -y
Cursor
$ npx skills add ramboz/jig --skill tdd-loop -a cursor -y
Gemini CLI
$ npx skills add ramboz/jig --skill tdd-loop -a gemini-cli -y
GitHub Copilot
$ npx skills add ramboz/jig --skill tdd-loop -a github-copilot -y
OpenCode
$ npx skills add ramboz/jig --skill tdd-loop -a opencode -y
Windsurf
$ npx skills add ramboz/jig --skill tdd-loop -a windsurf -y
SKILL.md
tdd-loop.SKILL.mdname: tdd-loop
description: >
Drive the red-green-refactor loop for any non-trivial code change. Auto-detect
the project's test runner (pytest / vitest / jest / node --test), invoke it via the
`tdd.py` helper, and act on the normalized exit code. Use when you write a
test, TDD this feature, let me test-drive a behaviour, are about to
implement [feature], want to run my tests, ask is my coverage complete, or
ask are tests green. Do not use for one-off exploratory scripts, throwaway
spikes, or pure-documentation edits that touch no executable code.
user-invocable: true
> Spec 006 promoted this skill from a dangling auto-install reference to an > active skill. The deterministic runner detection + subprocess invocation > live in `tdd.py`; this SKILL.md drives the judgment layer.
What this skill does
Codifies the red-green-refactor loop that the `implementer` subagent (`agents/implementer.md`) already encodes as "non-negotiable TDD discipline." The skill:
- Detects the project's test runner from filesystem signals (one of `pytest`,
`vitest`, `jest`, or Node's built-in `node --test`).
- Invokes the runner as a subprocess against a target, a focused
`--test-path`, or a single `--test` selector.
- Streams the runner's stdout/stderr through to the caller — you see real
output, not a swallowed summary.
- Normalizes the runner's native exit code so callers can branch
deterministically:
- `0` — all green
- `1` — at least one red test (runner started but reported failure)
- `2` — could not detect a runner, OR the runner binary is missing
The red-green-refactor loop
1. **Red.** Write a failing test for one acceptance criterion (or one well-bounded behaviour). Run `tdd.py run <target>` and confirm the new test is the *only* new failure — exit 1. 2. **Green.** Write the minimum implementation that makes the test pass. Run `tdd.py run <target>` again; expect exit 0. 3. **Refactor.** Clean up only after green. Re-run after each meaningful refactor; exit 0 must hold.
Stay in this loop one AC at a time. Do not write three tests and then three implementations — the loop's value is the tight feedback cycle.
When a test you did NOT just write goes red
The Green step above is about the *new* failing test you wrote for this AC — making it pass is the work. A **pre-existing** test going red is the opposite signal: it is a guard reporting that your change touched something load-bearing. The reflex to "make it green" is wrong here, and an agent is very good at satisfying it — deleting the assertion, loosening the expectation, or marking the test skipped all turn the light green while removing the protection.
So: **when a test you did not just write fails, do not fix the test — first suspect that your change broke what the test guards.** Only after you have ruled that out (the test encodes an intent that has genuinely, deliberately changed) may you touch the test, and then update its assertion *and its rationale* together so the next reader sees why. Never weaken, skip, `xfail`, or quarantine a pre-existing test to reach green. (This mirrors the standing rule for driving a PR to green — a red guard is work now, not an obstacle to route around.)
Helper invocations
Two subcommands cover the loop: `tdd.py detect` figures out which runner the project uses, and `tdd.py run` invokes it.
Detect the runner
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" detect [target]- `target` defaults to `.` when omitted.
- Stdout: `pytest`, `vitest`, `jest`, or `node` (one line).
- Exit 2 with stderr `no test runner detected at <target>` if no signal matches.
Signals checked (priority order — first hit wins):
- **pytest** — `pytest.ini` OR `conftest.py` OR `[tool.pytest` in
`pyproject.toml` OR a `test_*.py` / `*_test.py` file at root or in any direct subdirectory.
- **vitest** — `vitest.config.{ts,js,mjs}` OR `vitest` in `package.json`'s
`dependencies` / `devDependencies`.
- **jest** — `jest.config.{ts,js,json}` OR `jest` in `package.json`'s
`dependencies` / `devDependencies`.
- **node** — `package.json` `scripts.test` invokes `node --test`, OR a shallow
JS/TS file imports from `node:test`.
When multiple runners are detected, priority is **pytest > vitest > jest > node**.
Run the suite
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" run [target] [--test-path PATH]
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" run [target] [--test SELECTOR]- Auto-detects the runner via the same logic as `detect`.
- Default commands:
- pytest → `python3 -m pytest <path>`
- vitest → `npx vitest run <path>` (note the `run` — keeps watch mode off)
- jest → `npx jest <path>`
- node → `node --test` for the full suite; `node --test <path>` with
`--test-path`
- Output streams through to the caller's terminal.
- Exit code is normalized (0 / 1 / 2) per the table above.
Use `--test-path` when you want to run a focused subset (e.g. a single test file) while still letting the helper detect which runner to invoke. Use `--test` when you want one named test: pytest selectors are passed as node ids (`path::test_name`), vitest/jest selectors map to file plus `-t`, and node selectors map to file plus `--test-name-pattern` when shaped as `path::test name`.
When NOT to use
- **One-off scripts** or **REPL exploration** — TDD overhead isn't worth it
for code that won't survive the session.
- **Exploratory spikes.** Spikes are research artifacts; they answer "what
shape should this take?" — tests come *after* the spike has settled the shape.
- **Pure-documentation edits** that touch no executable code.
- **Auto-generated code** (codegen output, vendored deps). Test the
generator, not the generated artifact.
Sibling helper: `quality.py` (test-quality snapshot)
Alongside `tdd.py` the skill ships `quality.py`, a deterministic test-quality preflight. It reads a unified diff (`--diff-file PATH` or `--again
Read more
name: tdd-loop description: > Drive the red-green-refactor loop for any non-trivial code change. Auto-detect the project's test runner (pytest / vitest / jest / node --test), invoke it via the `tdd.py` helper, and act on the normalized exit code. Use when you write a test, TDD this feature, let me test-drive a behaviour, are about to implement [feature], want to run my tests, ask is my coverage complete, or ask are tests green. Do not use for one-off exploratory scripts, throwaway spikes, or pure-documentation edits that touch no executable code. user-invocable: true
> Spec 006 promoted this skill from a dangling auto-install reference to an > active skill. The deterministic runner detection + subprocess invocation > live in `tdd.py`; this SKILL.md drives the judgment layer.
What this skill does
Codifies the red-green-refactor loop that the `implementer` subagent (`agents/implementer.md`) already encodes as "non-negotiable TDD discipline." The skill:
- Detects the project's test runner from filesystem signals (one of `pytest`,
`vitest`, `jest`, or Node's built-in `node --test`).
- Invokes the runner as a subprocess against a target, a focused
`--test-path`, or a single `--test` selector.
- Streams the runner's stdout/stderr through to the caller — you see real
output, not a swallowed summary.
- Normalizes the runner's native exit code so callers can branch
deterministically:
- `0` — all green
- `1` — at least one red test (runner started but reported failure)
- `2` — could not detect a runner, OR the runner binary is missing
The red-green-refactor loop
1. **Red.** Write a failing test for one acceptance criterion (or one well-bounded behaviour). Run `tdd.py run <target>` and confirm the new test is the *only* new failure — exit 1. 2. **Green.** Write the minimum implementation that makes the test pass. Run `tdd.py run <target>` again; expect exit 0. 3. **Refactor.** Clean up only after green. Re-run after each meaningful refactor; exit 0 must hold.
Stay in this loop one AC at a time. Do not write three tests and then three implementations — the loop's value is the tight feedback cycle.
When a test you did NOT just write goes red
The Green step above is about the *new* failing test you wrote for this AC — making it pass is the work. A **pre-existing** test going red is the opposite signal: it is a guard reporting that your change touched something load-bearing. The reflex to "make it green" is wrong here, and an agent is very good at satisfying it — deleting the assertion, loosening the expectation, or marking the test skipped all turn the light green while removing the protection.
So: **when a test you did not just write fails, do not fix the test — first suspect that your change broke what the test guards.** Only after you have ruled that out (the test encodes an intent that has genuinely, deliberately changed) may you touch the test, and then update its assertion *and its rationale* together so the next reader sees why. Never weaken, skip, `xfail`, or quarantine a pre-existing test to reach green. (This mirrors the standing rule for driving a PR to green — a red guard is work now, not an obstacle to route around.)
Helper invocations
Two subcommands cover the loop: `tdd.py detect` figures out which runner the project uses, and `tdd.py run` invokes it.
Detect the runner
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" detect [target]- `target` defaults to `.` when omitted.
- Stdout: `pytest`, `vitest`, `jest`, or `node` (one line).
- Exit 2 with stderr `no test runner detected at <target>` if no signal matches.
Signals checked (priority order — first hit wins):
- **pytest** — `pytest.ini` OR `conftest.py` OR `[tool.pytest` in
`pyproject.toml` OR a `test_*.py` / `*_test.py` file at root or in any direct subdirectory.
- **vitest** — `vitest.config.{ts,js,mjs}` OR `vitest` in `package.json`'s
`dependencies` / `devDependencies`.
- **jest** — `jest.config.{ts,js,json}` OR `jest` in `package.json`'s
`dependencies` / `devDependencies`.
- **node** — `package.json` `scripts.test` invokes `node --test`, OR a shallow
JS/TS file imports from `node:test`.
When multiple runners are detected, priority is **pytest > vitest > jest > node**.
Run the suite
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" run [target] [--test-path PATH]
python3 "${CLAUDE_PLUGIN_ROOT}/skills/tdd-loop/tdd.py" run [target] [--test SELECTOR]- Auto-detects the runner via the same logic as `detect`.
- Default commands:
- pytest → `python3 -m pytest <path>`
- vitest → `npx vitest run <path>` (note the `run` — keeps watch mode off)
- jest → `npx jest <path>`
- node → `node --test` for the full suite; `node --test <path>` with
`--test-path`
- Output streams through to the caller's terminal.
- Exit code is normalized (0 / 1 / 2) per the table above.
Use `--test-path` when you want to run a focused subset (e.g. a single test file) while still letting the helper detect which runner to invoke. Use `--test` when you want one named test: pytest selectors are passed as node ids (`path::test_name`), vitest/jest selectors map to file plus `-t`, and node selectors map to file plus `--test-name-pattern` when shaped as `path::test name`.
When NOT to use
- **One-off scripts** or **REPL exploration** — TDD overhead isn't worth it
for code that won't survive the session.
- **Exploratory spikes.** Spikes are research artifacts; they answer "what
shape should this take?" — tests come *after* the spike has settled the shape.
- **Pure-documentation edits** that touch no executable code.
- **Auto-generated code** (codegen output, vendored deps). Test the
generator, not the generated artifact.
Sibling helper: `quality.py` (test-quality snapshot)
Alongside `tdd.py` the skill ships `quality.py`, a deterministic test-quality preflight. It reads a unified diff (`--diff-file PATH` or `--again
A Claude Code and Codex plugin that scaffolds AI-native development practices into new projects. jig adds a repeatable spec, implementation, review, and memory workflow to AI-assisted software projects.
Other skills on jig.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about tdd-loop.
- Which coding agents can use tdd-loop?
- tdd-loop installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

