tdd-loop
Skill7 starsChanged 5 months agoMITView on GitHub
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.
- Installs packages
9 other projects use a version of this.
Adopted by
Most recently taken up by
- milad-ahmd/agent-skillstheir own version, first seen Nov 8, 2026
- OrionArchitekton/orion-skillstheir own version, first seen Nov 2, 2026
- michaelalber/ai-toolkittheir own version, first seen Nov 1, 2026
- daniel-kindl/dkstacktheir own version, first seen Nov 1, 2026
- sbradl/claude-skillstheir own version, first seen Oct 15, 2026
- dagarre00/claude-code-templatetheir own version, first seen Oct 15, 2026
Other versions9 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 9
- khuynh22/agent-dev-team★ 9same name
- milad-ahmd/agent-skills★ 5same name
- michaelalber/ai-toolkit★ 3same name
- OrionArchitekton/orion-skills★ 2same name
- qyndex/claude-init★ 2same name
- dagarre00/claude-code-template★ 1same name
- daniel-kindl/dkstack★ 1same name
- itsmuskanagarwal/claude-pack★ 0same name
- sbradl/claude-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository21 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- adr-workflowSkill
- analyzeSkill
- arch-reviewSkill
- bug-fixSkill
- clarifySkill
- code-healthSkill
- contractsSkill
- explainSkill
- independent-reviewSkill
- memory-syncSkill
- migrateSkill
- orientSkill
- pr-reviewSkill
- reframeSkill
- scaffold-initSkill
- security-reviewSkill
- slice-landSkill
- spec-workflowSkill
- vision-elicitationSkill
The file
We split this skill into the rules it gives an agent
7 rules. 0 are common, 2 are distinctive, and 5 are very specific to this project.
0
Common
Most of the 9 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 38
Red: Write a failing test for one acceptance criterion (or one well-bounded behaviour).
- Very specific: line 41
Green: Write the minimum implementation that makes the test pass.
- Very specific: line 42
Run tdd.py run <target> again.
- Distinctive: line 93
When multiple runners are detected, priority is
- Very specific: line 113
Use --test-path when you want to run a focused subset (e.g.
- Very specific: line 115
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 …
- Distinctive: line 141
After the red-green-refactor loop, the independent-review skill's implementation-pass prompt embeds this snapshot verbatim so the reviewer …
“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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-innode --test). - Invokes the runner as a subprocess against a target, a focused
--test-path, or a single--testselector. - 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 green1— 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
- 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. - Green. Write the minimum implementation that makes the test pass.
Run
tdd.py run <target>again; expect exit 0. - 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]targetdefaults to.when omitted.- Stdout:
pytest,vitest,jest, ornode(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.iniORconftest.pyOR[tool.pytestinpyproject.tomlOR atest_*.py/*_test.pyfile at root or in any direct subdirectory. - vitest —
vitest.config.{ts,js,mjs}ORvitestinpackage.json'sdependencies/devDependencies. - jest —
jest.config.{ts,js,json}ORjestinpackage.json'sdependencies/devDependencies. - node —
package.jsonscripts.testinvokesnode --test, OR a shallow JS/TS file imports fromnode: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 therun— keeps watch mode off) - jest →
npx jest <path> - node →
node --testfor the full suite;node --test <path>with--test-path
- pytest →
- 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
--against REF), classifies test vs. code files (Python + JS/TS — pytest
/ vitest / jest), and emits a YAML snapshot with three numerical-ratio
signals (per-file-flood, assertion-thin, mock-heavy) plus the
underlying metrics. It is judgment fuel, not a gate — no exit-code
semantics other than 0 on success / non-zero on bad input.
After the red-green-refactor loop, the
independent-review skill's
implementation-pass prompt embeds this snapshot verbatim so the reviewer
sees the same deterministic signals the implementer just looked at.
The wiring is one-way (review.py shells out to quality.py); there is no
skill-router dispatch — quality.py is a helper, not a SKILL.
Relationship to the implementer subagent
agents/implementer.md enumerates TDD discipline as non-negotiable:
write the failing test first, then the minimum implementation, then refactor.
This skill is the tooling layer that implementer subagents (and Claude in
the main session) use to actually run the loop. The discipline lives in
agents/implementer.md; the deterministic invocation lives in tdd.py.
Gotchas
- The helper is detect-only for vitest/jest/node command mapping. Live
npx vitest/npx jestruns requirenode_modulesto be installed in the target; livenode --testruns require Node on PATH. Missing binaries exit 2 as environment errors, not red tests. Detection-only fixtures cover these JS runners in our own test suite; only pytest gets a real subprocess test because pytest is the runner with reliable presence in the jig dev environment. - Shallow scan depth. The
test_*.py/*_test.pyscan only checks the root and direct subdirectories (max depth 2 — per spec 001's signal-detection rules). Deep test trees (tests/unit/,tests/integration/) won't be found via the file-pattern path, butpytest.ini/conftest.py/pyproject.tomlcatch those projects anyway. - Exit code 1 vs 2. Exit 1 means the runner ran and saw red tests — inspect the streamed output. Exit 2 means the helper couldn't even get the runner running (no detection, missing binary). Don't conflate them.
- Detection logic is duplicated from
scaffold.py:_detect_testsby design (per ADR-0002 + slice 004-01 / 005-01 / 006-01 deviation logs). scaffold-init runs once at project setup and returns bool; tdd-loop runs every test invocation and returns the runner name. Extracting a sharedsignals.pywould couple two intentionally-independent lifecycles. tdd.pydoes not spawn the implementer subagent. It's a runner-driver, not a workflow orchestrator. Lifecycle transitions live inskills/spec-workflow/workflow.py.- Custom test command override (slice 006-05). Create
<target>/.jig/test-commandwith the first non-blank, non-comment line being the exact command to run. This takes priority over all runner auto-detection. Useful for projects whose test convention doesn't map to pytest/vitest/jest (e.g.python3 scripts/run_tests.py {test}for jig itself). - Targeted runs against a custom command are opt-in (bug 021). A
{test}token — as its own standalone argument, not embedded in another — marks where a--testselector is substituted; the token is dropped when no selector is passed. Without the token,run --test …exits 2 ("does not accept a test selector") instead of silently running the whole suite — a whole-suite exit code is not evidence about one named test, and the bug-fix red→green gates consume this exit code as exactly that. With the token, a non-zero exit whose output starts a line with a recognized no-match report (no matching tests,no test found,no tests found,your test suite must contain at least one test) maps to exit 2 (unresolved selector), mirroring the auto-detect runners — a custom command should emit one of those lines for a selector it cannot resolve (a command that exits 0 having matched nothing reads as green, same as the JS runners). - pytest module not installed (slice 006-04). If pytest is detected via
filesystem signals but the
pytestmodule isn't importable,runexits 2 with "not installed" in stderr — not exit 1 (red tests). Fix:pip install pytest. - Out of scope for slice 006-01: rspec /
go test/cargo testdetection, AC-to-test coverage mapping (deferred to 006-02), pre-commit hooks that block red commits (deferred to 006-03).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

