Use for any Standard, Complex, or Saga task once a spec exists, to run each behavior as one RED/GREEN/REFACTOR/REVIEW slice with the harness proving each step. In Standard the session writes each slice's failing test itself, under the lock, then implements it; in Complex and Saga the `test-author` subagent writes it and the implementer and slice-critic agents run as separate fresh contexts. Codex writes the test only when the owner opts in for that slice (R-907, owner decision 2026-09-23, IAN-333). Sits between writing-plans and subagent-driven-development; the Superpowers skills stay the skeleton, this skill is the enforcement.
Install
npx skillscat add nullvoidundefined/agent-governance/cursor-skills-tdd-gated-dispatch Install via the SkillsCat registry.
TDD-Gated Dispatch
One behavior at a time. The harness, not the prompt, proves RED and GREEN and
keeps the tests out of the implementer's hands.
Stack assumption: enforce/tdd.sh runs Vitest, Jest, pytest, or bash*.test.sh fixtures (the suite is every *.test.sh in the named files'
directories). Any other runner refuses; the loop below still applies by hand
until the runner lands.
Naming the RED tests. tdd.sh red <file> requires every test in the file
to fail, which fits a new test file. When the slice adds tests to a file that
already holds passing ones (a review fix is the usual case), name the new
tests by id instead: tdd.sh red 'tests/test_x.py::test_new' (also::TestClass::test_new, ::test_new[param-id], or a bare parametrized name
for every parameter set) under pytest, and tdd.sh red 'src/x.test.ts::<describe> <test title>'
(the full name -t matches) under Vitest and Jest. The named tests must fail
for the right reason and the file's other tests must keep passing, so a new
test imports a unit that does not exist yet inside its body, not at the top of
the file. Bash *.test.sh fixtures stay file-level, since each fixture is one
test; add a new fixture file instead. Never record RED by hand in a commit
message when an id would lock it.
What the harness enforces (so this skill does not have to ask for it)
| Step | Mechanism | Rule |
|---|---|---|
| No production edit before the failing test | .claude/tdd-lock.json phase open, hooks/protected-path-guard.sh |
R-412 |
| The test fails for the right reason, the rest is green | tdd.sh red |
R-412 |
| Tests, fixtures, and the spec are read-only from RED to close | lock phases red/green, the guard |
R-410 |
| Test author writes only tests; implementer never writes tests; critic writes nothing | agent_type against enforce/role-policy.json for the agents; tdd.sh validate test-author for a Codex run, which no hook sees |
R-411 |
| Named tests pass, suite at or above baseline, locked hashes match the RED commit | tdd.sh green |
R-412 |
| A writing subagent cannot return on a red suite | verification-gate.sh on SubagentStop |
R-509 |
What stays with you: choosing the slices, opening each one, writing the test
yourself (Standard) or dispatching the test author (Complex, Saga), committing
between RED and GREEN, recording a fallback, and arbitrating a DISPUTE:.
Slicing
Read the spec's acceptance criteria. Each criterion that a test can fail is a
slice, B-1, B-2, in the order the implementation needs them. A plan fromwriting-plans maps one task to one slice; its code blocks are a suggestion
for the implementer and are never shown to the test author (decision 2 of the
TDD harness assessment, docs/audits/2026-09-06-tdd-harness.md; every
"decision N" below is from that document). Before dispatching the test author,
write the slice's behavior line and criteria into the prompt as text; do not
paste the plan.
Too small to slice: pure pixel, spacing, or color decisions with no behavioral
component (the only R-705 exception). Everything else is a slice.
The loop, per slice
1. open bash ~/.claude/enforce/tdd.sh open "B-n <behavior>" --spec docs/superpowers/specs/<date>-<slug>-design.md
(Complex and Saga. In Standard there is no spec by design: open with the slice title alone,
`tdd.sh open "<behavior>"`, and read "the B-n entry" below as "the behavior in the slice title".
--spec is optional in tdd.sh; passing a path that does not exist locks a file nobody wrote.)
2. RED Standard: session writes the test. Complex/Saga: test-author agent (Codex on opt-in);
tdd.sh red <file | file::test id> prints RED:; tdd.sh validate test-author passes
3. commit git add <test file> .claude/tdd-lock.json && git commit -m "test(<scope>): B-n <behavior>"
4. GREEN implementer writes the minimum; tdd.sh green prints GREEN:
5. REFACTOR implementer, same lock; tdd.sh green again
6. commit git commit -m "feat(<scope>): B-n <behavior>" (or fix:, refactor:)
7. REVIEW slice critic returns findings and candidate tests
8. close tdd.sh close; accepted candidates become B-n+1Step 3 before step 4 is what makes tdd.sh green's hash check bind to a git
object: skip it and the check runs against the lock only, and says so.
Who writes the test (R-907)
| Tier | Default author | Codex |
|---|---|---|
| Standard | the session itself, under the lock, before implementing | only when the owner opts a slice in |
| Complex, Saga | the test-author agent, fresh context |
only when the owner opts a slice in; the agent is Codex's fallback |
Owner decision 2026-09-23 (IAN-333, "Drop the PR ceremony") replaced the 2026-09-19 rule that Codex wrote every test everywhere: each call cost 70,000-82,000 tokens, about 25,000 of it fixed startup, for independence the R-412 lock already proves in Standard.
When the owner opts in to Codex
Dispatch it from the orchestrating session with the test-author prompt below written to a scratch file (never the repository):
codex exec -s workspace-write -C <repo root> --skip-git-repo-check \
-o <scratch>/codex-B-n-final.md "$(cat <scratch>/codex-B-n-prompt.md)" \
</dev/null > <scratch>/codex-B-n.log 2>&1Run it with the Bash tool's run_in_background, and poll the log file until
the process exits; then read the final message file.
-s workspace-write, because Codex must write the test file; reviews use-s read-only.</dev/nullis required: with stdin open, codex blocks on "Reading
additional input from stdin" and never starts.- Never pipe the output through
tail; it buffers until exit and looks like
a hang. The log file is the progress signal. - No
-mflag:gpt-5.1-codex-miniis rejected on the owner's ChatGPT
account, and the account default is the right model. - R-908's billing guard fires on the command; it asks only when the call
would bill the metered API instead of the ChatGPT plan.
uv and pytest inside the workspace-write sandbox
-s workspace-write makes the repository writable and leaves the rest of the
filesystem read-only, with $TMPDIR and /tmp as the only other writable
roots (Codex has sandbox_workspace_write.exclude_tmpdir_env_var andexclude_slash_tmp to close even those, and this harness sets neither). uv's
cache sits outside all of them: run uv cache dir to see where, rather than
assuming a path, since it is ~/.cache/uv on this machine and uv does not put
it under ~/Library/Caches on macOS the way many tools do. The firstuv run pytest inside the sandbox therefore fails withOperation not permitted on the cache directory and Codex cannot run the
Python test it has just written. That happened on 2026-09-19 in
template-fastapi-nuxt, and a test author that cannot run the test is reduced to
guessing that it fails.
Point the cache at a path the sandbox already allows, and set it inside the command Codex runs rather than in the shell that launches codex exec. An inline assignment on the command line always reaches the sandboxed shell; a variable exported by the parent is filtered by shell_environment_policy, whose default does not pass an arbitrary name through, so do not rely on exporting UV_CACHE_DIR before the call:
UV_CACHE_DIR=.uv-cache uv run pytest tests/services/test_score_posting.pyWrite that form into the test-author prompt, in the same sentence that tells
Codex how to run the test, rather than leaving it to be rediscovered. Add.uv-cache/ to the project's .gitignore; UV_CACHE_DIR="$TMPDIR/uv-cache"
works equally well and leaves nothing in the working tree at all.
Two variants cover what the inline form does not:
-c shell_environment_policy.set.UV_CACHE_DIR=".uv-cache"on thecodex execcommand sets the variable for every command in the run, which is worth
the extra flag when the prompt drives more than a couple of test invocations.--add-dir "$(uv cache dir)"widens the sandbox to the real cache instead,
which keeps the cache warm between runs at the price of granting write access
outside the repository. Prefer the in-workspace cache unless a cold cache is
genuinely the bottleneck.
When uv also has to download an interpreter it writes to the pathuv python dir prints, which the sandbox blocks the same way. Either setUV_PYTHON_INSTALL_DIR next to the cache, or run uv python install once
outside the sandbox before dispatching, so the interpreter is already there.
Before dispatching, record the commit and the lock's hash:git rev-parse HEAD and shasum -a 256 .claude/tdd-lock.json. When Codex
returns, prove what it did rather than trusting its summary:
git rev-parse HEADstill prints the recorded commit.tdd.sh validate
reads only uncommitted changes, so a commit Codex made would hide any
production file inside it.- The lock's hash is unchanged.
tdd.sh validateleaves the lock out of its
path check, so an edited lock (a narrowed protected-path list) would pass
it and later letgreentrust the tampered lock. The prompt tells Codex
not to runtdd.sh, becausetdd.sh reditself rewrites the lock (phaseopentored); this check therefore runs before step 3, never after it. - Only then does the orchestrator run
tdd.sh reduntil it printsRED:,
naming a new test file whole (tdd.sh red <test file>) and each new test
in a file that already held passing tests by id (tdd.sh red '<test file>::<test id>', one argument per new test, using the ids Codex
reported), thentdd.sh validate test-author, proving every changed path
is a test or fixture path.
Codex runs the harness hooks through codex/hooks/codex-hook-adapter.sh, but
that adapter covers the file it is shown and not the session as a whole: it
sees no tdd.sh phase transition and no commit Codex makes for itself, so
these three checks remain the only proof of what the run actually did. If any
fails, discard the run: reset to the recorded commit if Codex
committed, restore the lock and every non-test path, commit nothing from it,
and re-run Codex or take the fallback.
Fallback. The owner's Codex account is a $20 ChatGPT plan with tight
usage limits. When an opted-in run's CLI is missing, unauthenticated, or
rate-limited (a usage-limit error in the log counts), dispatch thetest-author agent with the same prompt instead and name the fallback in
the PR body's test section as Test author: test-author subagent (fallback: <reason>). The codex-test-author-guard stays silent for that agent, silent
on a Standard or Trivial ledger, and asks whenever a Complex or Saga session
targets a test file itself outside an opted-in slice.
Standard tier: one session
You are the orchestrator and the implementer under the same lock. Write the
test yourself between open and red (R-907), then implement; no dispatch,
unless the owner opted this slice into Codex, dispatched as above. The guard
enforces the order: after open, a production write is denied until red
has run; after red, a test write is denied until close. Run the loop
exactly as above. Dispatch the critic only when the slice touches auth,
money, concurrency, or an external call.
Complex and Saga: three roles, fresh context each
The test author is the test-author agent (Opus, per its role file); Codex
only on owner opt-in, dispatched as above, with the agent as its fallback.
Dispatch the implementer, the critic, and the test author with the Agent
tool, subagent_type naming the role file in ~/.claude/agents/, and the
model the role file sets (test author and critic on Opus, implementer on
Sonnet; 2026-09-06 decision 5). Every prompt carries paths, not content
(R-701), and the branch block from ~/.claude/prompts/subagent-branch-setup.md
(R-702) for cd only; neither Codex nor the agents commit, you do.
Test author prompt (sent to the test-author agent; the same text goes to
Codex when the owner opts the slice in; in Standard, the session follows it
directly instead of dispatching):
## Slice
B-n: <behavior line from the spec>
## Spec
<absolute path to the spec>; read the B-n entry, its criteria, invariants, and failure modes.
## Conventions
<absolute path to CLAUDE-BACKEND.md or the project's test conventions>
## Branch
<R-702 block>
## Boundary
Write only test and fixture files. Read only the spec entry, the conventions file, and the existing code the test must call; do not scan the rest of the repository.
## Definition of done
The `test-author` agent: `bash ~/.claude/enforce/tdd.sh red <test file>` prints RED: (name `<test file>::<test id>` for each new test when the file already holds passing tests). Codex, on owner opt-in: write the test and run it once with the project's test runner to see it fail for the right reason, but do not run `tdd.sh` (the orchestrator runs `tdd.sh red`, which moves the lock, after checking the lock is untouched). Either way, report the test file path, whether the file is new or already held passing tests, and for an existing file the id of every test you added (the pytest node id after `::`, or the Vitest or Jest full name), then the failure you saw, the failure class, every interface the test assumes, and any spec ambiguity you resolved. Do not implement. Do not commit.Implementer prompt (after the RED commit):
## Slice
B-n: <behavior line>
## Contract
Tests: <absolute test path(s)>. Lock: <repo>/.claude/tdd-lock.json. Spec: <absolute spec path>, the B-n entry.
<optional: plan task N at <absolute plan path>, a suggestion; the tests decide>
## Branch
<R-702 block>
## Definition of done
`bash ~/.claude/enforce/tdd.sh green` prints GREEN:. Then refactor and run it again. Report the GREEN line, files changed, reuse found (R-308), and anything the spec asks for that no test covers. If a test is wrong, return `DISPUTE: <file>: <title>: <why>` and stop. Do not edit tests, fixtures, the spec, or the lock. Do not commit.Critic prompt (after the GREEN commit):
## Slice
B-n: <behavior line>
## Inputs
Spec: <absolute spec path>. Tests: <absolute test path(s)>. Diff: git diff <RED commit sha>...HEAD.
## Output
The seven questions in your role file, with file:line evidence or "none found", then ## Candidate tests as behavior statements. Write nothing.Validating a return
One command per role; a non-zero exit means the slice is not done:
bash ~/.claude/enforce/tdd.sh validate test-author # phase red; only test and fixture paths changed (the test-author agent, or Codex on opt-in)
bash ~/.claude/enforce/tdd.sh validate implementer # phase green; no test, fixture, spec, or lock writes; GREEN re-run
bash ~/.claude/enforce/tdd.sh validate slice-critic # nothing written at allIt reads the boundary from enforce/role-policy.json (R-411) and judges git status --porcelain against it, so the orchestrator never applies the patterns by eye; tdd.sh status still prints the lock when the refusal needs context. Run the implementer's validate before the GREEN commit, the critic's after it.
The author's own mistake. When whoever wrote this slice's RED test (you in
Standard, the test author in Complex and Saga) finds a bug in it while the
phase is still red, before any GREEN and before the RED is pushed, fix it
with tdd.sh amend instead of a DISPUTE::
bash ~/.claude/enforce/tdd.sh amend <test file> # phase amending: only this file is writable
# fix the test
bash ~/.claude/enforce/tdd.sh amend <test file> # must still fail for a classified reason; back to redThe second run re-hashes the file and records the amendment in the lock
(amendments, with git blobs of the test before and after, sogit diff <fromBlob> <toBlob> shows the change); commit the amended test
before the implementation. It is refused for a test from an earlier slice,
after green, and once the RED is pushed. A dispatched implementer agent can
never amend, since role policy denies it every test write; in Standard, where
one session writes both, the only brake is the still-failing requirement and
the recorded diff, so amend to fix the test's own mistake, never to make the
implementation easier. Weakening a test is a DISPUTE:.
A DISPUTE: return stops the loop. Show the user the test, the claim, and the
spec line. If the user agrees the test is wrong, tdd.sh cannot unlock it: the
user deletes .claude/tdd-lock.json outside the session, you open the slice
again, and the test author writes the corrected RED. Never edit the test
yourself.
A lock from a dead session. tdd.sh open refuses while another slice's
lock exists. When that lock's session is gone, run tdd.sh abandon rather than
asking the user to delete it: it closes the lock and logs the close only when
the lock recorded a test, nothing has run tdd.sh there forCLAUDE_TDD_STALE_HOURS (default 4), no live process outside this session
works in the tree, and every locked test is committed and passing with the rest
of the suite green. Any refusal names what is missing; unfinished work means
resume the slice, and anything else goes to the user.
Common mistakes
| Mistake | What happens now |
|---|---|
| Writing production code "while the test is fresh in mind" | denied by the guard until tdd.sh red has run |
| Handing the test author the plan with its code | the test mirrors the plan's misunderstanding; strip the code, pass the behavior line |
| Writing the test in-session because dispatch feels slow | correct in Standard; in Complex/Saga the codex-test-author-guard asks, dispatch the test-author agent instead |
Committing an opted-in Codex RED without tdd.sh validate test-author |
Codex runs outside the hooks, so a production file it wrote would ride into the RED commit unseen |
Running codex exec with stdin open or piped through tail |
it blocks on stdin, or looks hung until exit; close stdin and poll the log file |
| Letting the implementer "fix" a flaky assertion | denied; the only path is DISPUTE: |
| Skipping the RED commit | tdd.sh green hash-checks against the lock only and warns |
| Asking the user to delete the lock over a typo in the test you just wrote | tdd.sh amend <test file> twice, while still red |
| Deleting a stale lock by hand | denied (R-410); tdd.sh abandon closes it when its tests are committed and passing |
| Dispatching the critic with the implementer's summary | anchored review; give it paths and the diff range only |
| Batching three behaviors into one slice | three tests, one implementation, no minimal step; slice again |
| Using this skill for exploratory or research work | nothing to assert; use plain dispatch |
Refactor-only work
A behavior-preserving change has no RED to prove, so the green suite is the
contract:
bash ~/.claude/enforce/tdd.sh open --refactor "R-n <what changes>" --lock <test files that pin the behavior>The suite must be green at open; the named test files (every test file the
suite ran when none is named) are locked and hashed; tdd.sh green requires
the same tests to pass unchanged and the outside count not to drop; tdd.sh red is refused until the slice closes, because a new behavior is a new slice.