| name | tdd |
| description | Implement features using Test-Driven Development (TDD) with Red-Green-Refactor cycle. |
| disable-model-invocation | true |
Test-Driven Development
Implement $ARGUMENTS using Test-Driven Development (TDD).
TDD Cycle
Repeat: Red → Green → Refactor
1. Red: Write a failing test
2. Green: Write minimal code to pass the test
3. Refactor: Clean up code (tests still pass)
Which test cases matter, and when to refactor, is judgment. The one thing that
is not judgment is whether a run came out the way you expected: every test run
in this skill goes through .agents/skills/_shared/run_tests.py, which turns
"confirm failure" / "confirm success" into an exit code.
Who Does What (delegation-first)
Per .agents/rules/delegation.md, the lead does not write the cycles by hand.
The split is fixed:
| Work | Owner |
|---|
| Requirement clarification, test-case list, boundary values | Lead (judgment, Self-Handle List item 5) |
| Writing the tests and the production code, cycle by cycle | general-purpose-sonnet |
| Cycles with ambiguous design, cross-cutting invariants, or security / concurrency / data-integrity risk | general-purpose-opus |
| A Red that is red for the wrong reason, or a Green that will not go green after one retry | codex-debugger |
Every run_tests.py / verify.sh gate and the final report | Lead — verification never delegates |
Only a single trivial cycle on a file already open in context stays with the
lead (Self-Handle List item 2). Two or more cycles, or a module the lead has not
read, is delegated. Independent modules are delegated in parallel in one
message; they share no test file, so nothing serializes them.
The Red/Green Invariant
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect fail --label red-1
--expect fail|pass (required) and --target PATH (repeatable, at least one)
are the whole interface; --label names the log file under .agents/logs/
(default run-tests), so a later cycle does not overwrite an earlier log.
Read observed in the JSON — a non-zero exit is not all the same red:
observed | pytest exit | What it actually means |
|---|
failed | 1 | The test ran and failed. This is the only valid Red. |
passed | 0 | Nothing new is being asserted — the test cannot drive any code. |
collection_error | 2 | The file did not import (syntax error, bad fixture, missing import): red for the wrong reason. Fix the test, not the production code. |
no_tests_collected | 5 | Nothing was selected — the test was never written, or the name does not match the discovery pattern. |
Exit codes: 0 observed matches --expect; 1 bad arguments — including a
--target that does not exist (the mistyped path, caught before pytest runs)
and a pytest usage error (exit 4, e.g. an unknown ::node id); 2 observed
does not match --expect, or coverage is below --min-coverage; 3 external
failure — no pytest runner available, a timeout (--timeout, default 600s), or
a pytest internal error. On 1 and 3 the payload's observed is null: the
script reports that it has no observation rather than guessing one.
Other payload fields: expected, runner, command, exit_code, summary,
failed_tests (node ids from the short summary), coverage_percent,
min_coverage, log_file, artifacts, error.
The runner is resolved, not hardcoded: uv run pytest when uv and a
pyproject.toml are present, otherwise pytest from PATH, otherwise
python -m pytest (probed for importability first). --runner pins one
explicitly. If none is available the result is exit 3, never a silent pass.
Implementation Steps
Phase 1: Test Design
-
Confirm Requirements
- What is the input
- What is the output
- What are the edge cases
-
List Test Cases — record them with TodoWrite (one todo per case), so the
remaining cases survive a context reset instead of living only in this
conversation:
- [ ] Happy path: Basic functionality
- [ ] Happy path: Boundary values
- [ ] Error case: Invalid input
- [ ] Error case: Error handling
Which cases to write, and which boundary values matter, is domain reasoning —
never delegate it to a script.
Phase 2: Red-Green-Refactor
Step 0: Hand the Cycles Off
Delegate the Red-Green-Refactor loop with the six-element prompt contract from
.agents/rules/delegation.md. One delegation per module, all launched together:
Task tool:
subagent_type: "general-purpose-sonnet" # or general-purpose-opus, see the table above
prompt: |
Objective: Implement {module} by strict TDD, one test case at a time.
Scope:
- Write only tests/test_{module}.py and src/{module}.py. Touch nothing else.
- Do not modify existing tests, and never skip, delete, or weaken an assertion.
Inputs:
- Test cases to implement, in this order: {the Phase 1 list}
- Read .agents/rules/coding-principles.md and .agents/rules/testing.md first.
- Existing conventions to follow: {paths of comparable modules}
Acceptance checks — run these yourself, per cycle, and never skip Red:
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect fail --label red-{n}
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect pass --label green-{n}
A Red that exits 2 with observed=passed/collection_error/no_tests_collected
is not a Red: fix the test, not the production code, and re-run.
Output shape:
## Cycles completed (case -> red label -> green label)
## Files changed
## Deviations from the requested test cases, and why
## Anything that did not go green
Context discipline: return this summary only; the run logs stay in
.agents/logs/ and are referenced by label, not pasted.
Then verify rather than trust: re-run run_tests.py --expect pass yourself and
read the diff. A reported cycle with no matching run_tests.py exit 0 did not
happen. If a cycle came back unfinished, escalate it (general-purpose-opus or
codex-debugger) instead of re-sending the same prompt to the same tier.
The steps below are the contract the delegate follows — and what the lead runs
directly in the single-trivial-cycle case.
Step 1: Write First Test (Red)
def test_{function}_basic():
"""Test the most basic case"""
result = function(input)
assert result == expected
Confirm the test is red for the right reason:
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect fail --label red-{n}
Exit 0 means it genuinely failed. Do not proceed to Green on any other exit
code: on exit 2 read observed (see the table above), on exit 1/3 read
error and log_file.
Step 2: Implementation (Green)
Write minimal code to pass the test:
- Don't aim for perfection
- Hardcoding is OK
- Just make the test pass
Confirm success:
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect pass --label green-{n}
Exit 0 means the test now passes. Exit 2 with observed: failed means the
implementation is not there yet — read failed_tests.
Step 3: Refactoring (Refactor)
Improve while tests still pass:
- Remove duplication
- Improve naming
- Clean up structure
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect pass --label refactor-{n}
When to refactor, and how far, stays a judgment call.
Step 4: Next Test
Return to Step 1 with the next test case from the Phase 1 list.
Phase 3: Completion Check
Run the full quality gates:
bash .agents/skills/_shared/verify.sh
Read the JSON: overall is pass / fail / no_gates. Exit 0 means
overall: pass; exit 2 means a gate failed or no gate could run at all —
inspect log_file and tools. no_gates is a failure by default because a
code-editing session must not be able to declare done with zero checks
executed; if the project genuinely has no configured gates, re-run with
--allow-no-gates and verify manually with the project's own commands, and say
in the report that you did so. Exit 1 is bad arguments, 3 the log file could
not be written.
Then check coverage — with a threshold, so the answer is a gate and not a glance
at term-missing output:
python3 .agents/skills/_shared/run_tests.py \
--target tests/test_{module}.py --expect pass \
--cov {module} --min-coverage {N} --label coverage
--cov MODULE (repeatable) requires the pytest-cov plugin; without it pytest
reports a usage error and run_tests.py exits 1 pointing at log_file.
Coverage below --min-coverage is exit 2; --cov with no coverage total in
the output is exit 3, never a silent pass. --min-coverage requires --cov.
Choosing the threshold is a project decision.
Report Format
## TDD Complete: {Feature Name}
### Test Cases
- [x] {test1}: {description}
- [x] {test2}: {description}
...
### Coverage
- {coverage_percent}% (threshold {min_coverage}%) — from run_tests.py, label `coverage`
### Quality Gates
- verify.sh: {overall} — `{log_file}`
### Implementation Files
- `src/{module}.py`: {description}
- `tests/test_{module}.py`: {N} tests
Every [x] above must correspond to a run_tests.py --expect pass run that
exited 0; mark nothing complete on the strength of having written it.
Notes
- Write tests first (not after)
- Keep each cycle small
- Refactor after tests pass
- Prioritize working code over perfection
- Where the production code belongs follows the project's existing layout — that
is a design decision, not a derived path