A Spec Kit extension

A green suite is not evidence. A recorded red is.

TDD Extension makes the implementation phase test-driven in any language. It turns acceptance criteria into a test list, drives red-green-refactor one behavior at a time while recording the failure that preceded each fix, then audits the result from cold context: was the test really first, does it assert behavior, and would it actually catch a bug.

Install with Spec Kit
specify extension add tdd \
  --from https://github.com/d0whc3r/spec-kit-tdd/releases/download/v1.1.2/tdd-1.1.2.zip

Works with zero configuration. This is the most reliable path and the same one you use to pin a version.

  • Four commands
  • Requires Spec Kit >= 0.11.9
  • MIT licensed
  • Any language

Why this exists

Spec-driven development gets the specification right and then hands it to an agent that writes the code and its tests in the same pass. That is where the guarantee leaks. Tests written next to the code they check tend to pass while proving very little, and coverage agrees with them, because every one of those tests executes the line. The suite goes green and the feature reads as done.

Ordering, recorded

Each cycle writes one test, runs it, and records the real failure output before the implementation exists. A test that fails on a typo is not a valid red, and a test that passes immediately gets a deliberate-mutant check. The audit re-checks that record against git history rather than trusting it.

Strength, measured

Coverage counts execution. Mutation testing asks whether a bug would have been caught, which is the only mechanical way to expose a tautological test that reads fine. It runs scoped to the files the feature changed, and degrades to deliberate mutants where the ecosystem has no tool.

Graded by someone else

A loop cannot grade itself: the session that wrote the tests fills every gap from memory. /speckit.tdd.verify runs cold, fails closed on missing evidence, and never fixes what it finds. Findings become remediation tasks, not silent edits.

Four commands

One per moment in the lifecycle, each with its own write boundary. /speckit.tdd.run is the only one that writes tests or source. The command that grades the work is deliberately not the command that does it.

Command What it does Writes
/speckit.tdd.setup Detects every test stack from the manifests, scripts, CI config, and test layout, then proves each command by running it. Once per repository. Also proposes the TDD principle for your project constitution. .specify/memory/tdd-profile.md, constitution (with approval)
/speckit.tdd.plan Turns acceptance criteria into outer-loop behaviors and plan components into inner-loop behaviors, each traced to what it serves. Then removes the optionality from the test tasks in tasks.md and orders each one before the implementation it covers. tdd/test-list.md, tdd/cycle-log.md, tasks.md
/speckit.tdd.run The loop. One failing test, red proven and recorded, smallest green, full suite, refactor while green, one commit, and the tasks that behavior covers ticked. Stops and reports rather than improvising when the suite is red at baseline or a criterion turns out ambiguous. tests, source, tdd/cycle-log.md, the tasks.md checkboxes it earned
/speckit.tdd.verify The audit, from cold context. Test-first evidence in git history, the test-smell rubric, mutation on the changed files, criteria coverage. Verdict: PASS, PASS_WITH_GAPS, FAIL, or BLOCKED. tdd/verification.md, remediation in tasks.md

Paths are relative to the feature directory Spec Kit resolves, usually specs/<feature>/.

Three hooks put the right command at the right moment, at the three moments the discipline is most often skipped. after_tasks offers plan and after_implement offers verify; both prompt, and you can decline. before_implement runs run and /speckit.implement waits for it, because Spec Kit only waits for a hook that is not optional, and a prompt arriving after the code was written would be worthless. The loop ticks the tasks it drove, so /speckit.implement covers only what is left. Disable any hook in .specify/extensions.yml.

Modifiers: refresh re-detects or re-derives, next stops the loop after one cycle, outer works the next acceptance behavior, tcr switches to test && commit || revert, quick and deep set the audit's depth, and branch audits everything the current branch changed.

See it in action

One real feature in a TypeScript service: session expiry plus scoping the order list to the requesting user. Three acceptance criteria, eleven cycles. On the left is what the loop recorded; on the right is what the cold audit found. Everything is rendered straight from the files in the examples folder. Pick a view, read the excerpt, and open the full version.

Loop output cycle-log.md
## Cycle 3: U2 accepts a token expiring exactly at the current instant

- test: `src/auth/session.test.ts::accepts a token expiring now` (new)
- red: `pnpm vitest run src/auth/session.test.ts -t "accepts a token expiring now"`
  -> `AssertionError: expected 'expired' to be undefined` (1 failed)
- green: `src/auth/session.ts:31` changed `<` to `<=`. Suite -> 126 passed
- refactor: extracted `isExpired(claims, now)` from the inline comparison; suite
  re-run green after the extraction
- commit: `9c2b117` (behavior), `5ee0a30` (structure)
- note: U1 alone passed with both `<` and `<=`, which is exactly why the
  boundary needed its own behavior on the list.

A real command, its real failure output, recorded before the fix existed. Behavior and structure land as separate commits.

Audit specs/003-user-auth/tdd/verification.md
---
feature: 003-user-auth
verdict: PASS_WITH_GAPS
behaviors: 12
proven: 9
likely: 2
test_after: 0
high_smells: 0
criteria_covered: 3
mutation_score: 87
mutants_survived: 3
---

Two behaviors are LIKELY rather than PROVEN: their commits were amended, so git history can no longer corroborate that the test came first. The log says it did. The audit will not upgrade that on the log's word alone.

The snippets above are excerpts. Hit View full file to read the whole artifact rendered in place, or browse every file on the Examples page in the wiki.

The same cycle in your language

Nothing above is TypeScript-shaped by accident. Here is that same behavior, U2, driven in five stacks. The test list row is identical in all five, word for word. Three things follow the ecosystem, and all three are copied out of the runner rather than written: the test reference, the command that produced the red, and the failure text.

Profile single: 'pnpm vitest run {file} -t "{name}"'
- test: `src/auth/session.test.ts::accepts a token expiring now` (new)
- red: `pnpm vitest run src/auth/session.test.ts -t "accepts a token expiring now"`
  -> `AssertionError: expected 'expired' to be undefined` (1 failed)
- green: `src/auth/session.ts:31` changed `<` to `<=`. Suite -> 126 passed
- refactor: extracted `isExpired(claims, now)`; suite re-run green
- commit: `9c2b117` (behavior), `5ee0a30` (structure)

The behavior sentence on the test list never changes. The test name inside the file does, and it follows whatever your repository already does: it("accepts a token expiring now"), test_accepts_token_expiring_now, TestAcceptsTokenExpiringNow. The loop copies that convention out of the exemplar test file recorded in your profile instead of imposing one, which is why no example test source ships with the extension. Full comparison on the Stack Profiles page.

Get started in five minutes

  1. Install the extension

    You need Spec Kit >= 0.11.9 initialized (specify init), a git repository, and a working test runner. Install directly from the latest release; this needs no catalog setup and is the recommended path:

    specify extension add tdd \
      --from https://github.com/d0whc3r/spec-kit-tdd/releases/download/v1.1.2/tdd-1.1.2.zip

    Confirm it registered:

    cat .specify/extensions/.registry   # 'tdd' entry present
    ls .specify/extensions/tdd          # extension files present

    Prefer specify extension add tdd by name? That needs the community catalog approved first. See install help.

  2. Set up the stack, once

    This detects every stack in the repository and runs each candidate command to prove it works. Read the report: the useful part is usually negative. A suite that is already red, no way to run a single test, or a package with no runner each change what you do next.

    /speckit.tdd.setup
  3. Derive the test list

    After the usual /speckit.specify, /speckit.plan, and /speckit.tasks. Then read tdd/test-list.md in the feature directory. This is the cheapest moment in the whole feature to catch a missing boundary case or a criterion nobody can test.

    /speckit.tdd.plan
  4. Run the loop, then audit it cold

    With no arguments the loop walks the whole list; next stops it after one cycle. It ticks the behavioral tasks as it goes, so /speckit.implement then covers only the scaffolding, configuration, and wiring that never belonged in a red-green cycle. Run the audit in a fresh session: its whole value is having no memory of the loop, so it reads what the tests actually say.

    /speckit.tdd.run
    /speckit.implement
    /speckit.tdd.verify

How it flows

Every artifact is plain Markdown committed alongside the code it describes. No database, no index file, no runtime. The loop never pushes, never merges, and never commits on red.

spec-kit core                     the extension                     artifacts
-------------                     -------------                     ---------

(once per repo)             -->  /speckit.tdd.setup    -->  .specify/memory/tdd-profile.md
                                 detect + prove             constitution principle (with approval)

/speckit.specify
/speckit.plan
/speckit.tasks              -->  /speckit.tdd.plan     -->  tdd/test-list.md (behaviors PENDING)
                                 (after_tasks hook)         tdd/cycle-log.md (baseline)
      |                                                     tasks.md (tests mandatory, ordered)
      v
/speckit.implement          -->  /speckit.tdd.run      -->  tests + source
  (waits for it)                 (before_implement hook)    tdd/cycle-log.md (one entry per cycle)
                                 red -> green               tdd/test-list.md (states -> DONE)
                                 -> refactor -> commit      tasks.md (behavioral tasks ticked)

/speckit.implement          -->  /speckit.tdd.verify   -->  tdd/verification.md (verdict)
  (after it finishes)            (after_implement hook)     tasks.md (remediation phase)

The outer loop is the feature's acceptance criteria, measured in hours. The inner loop is the components beneath them, measured in minutes. The acceptance test is written first and stays red until the feature works end to end, because it is the only test that fails when every unit is individually right and the composition is wrong.

Install help

If specify extension add tdd stops with the error below, this is the fix. It is expected behavior, not a broken release.

The error you may see

Error: 'tdd' is available in the 'community' catalog but installation
is not allowed from that catalog.

To enable installation, add 'tdd' to an approved catalog
(install_allowed: true) in .specify/extension-catalogs.yml.

Spec Kit ships the community catalog as discovery only. It carries install_allowed: false by design, so the CLI can list community extensions but will not install one until you opt in. You have two ways to opt in.

Option A: Direct install (recommended)

Install straight from the release archive. No catalog config, always works, and it is the only way to pin a specific version.

specify extension add tdd \
  --from https://github.com/d0whc3r/spec-kit-tdd/releases/download/v1.1.2/tdd-1.1.2.zip

To update later, run the same command with a newer version URL.

Option B: Approve the community catalog

Do this once if you want to install and update by name. It adds the catalog with install_allowed: true to .specify/extension-catalogs.yml.

specify extension catalog add \
  https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json \
  --name community --install-allowed

specify extension add tdd
specify extension update tdd

Community extensions are author-maintained and not reviewed by Spec Kit. Review the source before approving a catalog.

More refusal codes and fixes live in Troubleshooting.

Frequently asked questions

Why does specify extension add tdd fail?

The extension lives in Spec Kit's community catalog, which is discovery only and carries install_allowed: false. The CLI can list it but will not install until you opt in. Either install directly with --from <release-zip-url>, or approve the community catalog once. Full steps are in install help.

Why not just tell the agent to write tests first?

Because that is an instruction, and what you need is evidence. An agent asked to be test-driven will produce a test file and an implementation in one pass and sincerely describe the result as test-driven. Nothing in the output distinguishes that from a real cycle. What makes it checkable is the ordering being recorded as it happens, and a separate pass with no memory of the session cross-checking that record against git history.

Which language does it support?

All of them, because it does not know any of them. /speckit.tdd.setup detects the stack from your manifests, scripts, CI config, and test layout, runs each command to prove it works, and writes them to a profile the other commands read. Starting points are documented for JS and TS, Python, JVM, .NET, Go, Rust, Ruby, PHP, Swift, Elixir, C and C++, and Dart, along with their coverage, mutation, and property-based tooling. Detection always wins over the table.

That is a claim worth checking rather than believing, so the same cycle is shown above in five ecosystems. The behavior text on the test list is identical in all five; three lines per cycle differ, and all three are quoted from the runner rather than composed.

Will it edit my tests to make them pass?

No. That is the hardest rule in the extension: no loosened assertion, no widened tolerance, no value check turned into a truthiness check, no skip, no narrowed filter, no lowered threshold. When a test and the code disagree, spec.md decides which is wrong. The audit checks the branch diff for exactly these edits, and any of them is a FAIL condition on its own.

Does it work on legacy code with no tests?

That case is planned for explicitly. A component the feature must change but which has no tests gets characterization behaviors first: tests that capture what the code does today, including behavior that looks wrong, as a baseline. They are verified with a deliberate mutant so a vacuous baseline cannot slip through, and they are scheduled before the behaviors that change that component.

Is mutation testing required?

No, and it is scoped when it runs: only the files the feature changed, with the scope reported alongside the score so numbers stay comparable. Where the ecosystem has no mutation tool, the audit uses deliberate mutants on the highest-risk behaviors instead, and states how many were sampled so the report cannot read as exhaustive.

I squash my branches. Does that break it?

It costs you the strongest evidence. The audit's PROVEN class needs git history to corroborate that the test changed with or before the source, and a squashed branch loses that ordering. Those behaviors become LIKELY, which does not block a pass but caps the verdict at PASS_WITH_GAPS. Keep the per-cycle commits until after the audit and squash on merge instead.

How does it relate to the BDD and testing extensions?

It composes with them rather than replacing them. Gherkin scenarios from the bdd extension become outer-loop behaviors, generated scaffolds become list items the loop still has to drive to red, and existing traceability or coverage-drift reports become corroboration in the audit. The gap this fills is the loop discipline and the test-strength check, which nothing else in the catalogue does.

Does the extension run by itself?

No. The commands are Markdown prompts. They need a Spec Kit-aware assistant to resolve and execute them. The release zip is portable and has no runtime of its own.

How do I update the extension?

If you approved the community catalog, run specify extension update tdd. Otherwise rerun the direct install with the newer release URL. Your specs/ tree and your stack profile are not touched.

More answers in the full FAQ.