A small Pi extension for deterministic TDD loop engineering.
For coding tasks, pair tdd_loop with Pi's tdd skill. The loop's job is simple:
Codify expected functionality as behavior tests, one vertical slice at a time — writing the test and the code in the same pass — then loop until the test command and configured validation pass.
Avoid bulk-writing all tests first. Prefer TDD tracer bullets: one failing behavior test → minimal implementation → passing verifier plus validation → next behavior.
Each TDD iteration includes a test-quality contract: identify the behavior slice, consider likely failure modes and relevant edge cases, write one public-interface behavior test, confirm the red phase fails for the expected reason when practical, then implement only the smallest fix.
Clone into Pi's global extension directory:
git clone https://github.com/k1000/tdd_loop.git ~/.pi/agent/extensions/tdd_loopThen restart Pi or run:
/reload/tdd_loop <name-or-path> # create/edit if needed, then start; resumes unfinished runs automatically
/tdd_loop fresh <name-or-path> # force a new run instead of auto-resuming
/tdd_loop status # show active loop status
/tdd_loop stop # stop active loop
/tdd_loop edit <name-or-path> # edit an existing specThe Pi UI widget shows live status while a loop runs.
The package includes a skill:
/skill:tdd_loopUse it to design deterministic loop specs and turn expected coding functionality into test-based completion criteria.
| Task | Shape | Key fields |
|---|---|---|
| Small, one test | no steps |
prompt, check, attempts |
| Medium, one suite | no steps |
loop until the suite passes |
| Large, ordered steps | steps[] without dependsOn |
steps[], attempts, finalCheck? |
| Large, independent parts | steps[] with dependsOn |
steps[] with dependsOn, parallelism |
| Fuzzy / subjective | any | first loop writes the test or checker, then stop for human approval |
{
"name": "fix-login-bug",
"goal": "Fix the login bug",
"prompt": "First write or update one behavior-focused failing test that proves the login behavior and its predictable failure mode. Then implement the smallest fix. End by calling tdd_loop_report.",
"check": "npm test -- login.test.ts",
"validationCommand": "fallow audit --format json --quiet --fail-on-issues",
"attempts": 5
}Steps run strictly in order unless any step has dependsOn; then independent tasks unlock as their dependencies pass, only ready tasks are surfaced per iteration, and the agent picks one. Legacy plan still parses as an alias for steps.
{
"name": "onboarding-parallel",
"goal": "Implement independent onboarding pieces",
"parallelism": 2,
"attempts": 3,
"steps": [
{ "id": "draft", "name": "Creates draft applicant", "dependsOn": [], "prompt": "Write/update the draft test, then implement the smallest fix.", "check": "npm test -- onboarding.create-draft.test.ts" },
{ "id": "validate", "name": "Validates required fields", "dependsOn": [], "prompt": "Write/update the validation test, then implement the smallest fix.", "check": "npm test -- onboarding.validation.test.ts" },
{ "id": "kyc", "name": "Submits to KYC", "dependsOn": ["draft"], "prompt": "Write/update the KYC test, then implement the smallest fix.", "check": "npm test -- onboarding.kyc-submit.test.ts" }
],
"finalCheck": "npm test -- onboarding"
}Drop dependsOn for the strictly-ordered variant.
/tdd_loop <name> sends an iteration prompt. The agent does one focused unit of work — write/update one failing test, implement the smallest fix — then calls tdd_loop_report with progress only:
{
"summary": "Added login regression test and fixed token parsing.",
"blocked": false,
"nextPrompt": "If still failing, inspect cookie handling.",
"artifacts": ["tests/login.test.ts", "src/auth.ts"]
}Before reporting, the agent should check that the new test would fail against the old/broken behavior, avoid implementation-coupled assertions, and mention relevant edge cases intentionally deferred.
The extension then runs the current verifier, then the configured Fallow validation pass, and decides:
verifier + validation exit 0 => step/task done, unlock next eligible work, or finish loop
nonzero verifier/validation => return the failure reason to the same task, continue same step/task, or stop at maxIterations
blocked true => blockedIn dag-plan mode, the agent may set tdd_loop_report.taskId to choose among currently-ready independent tasks. The agent never decides done; passing the current verifier and Fallow validation is the completion proof.
Delegation is useful for dag-plan loops, but the loop verifier remains the source of truth.
Recommended pattern:
- The main agent decomposes the goal into DAG steps with explicit
dependsOn, narrowchecks, and expected file ownership in eachprompt. - The main agent may delegate ready DAG tasks to subagents only when those tasks are independent and file ownership is disjoint.
- Each subagent works on one behavior slice: write/update one public-interface behavior test, implement the smallest fix, run the step verifier, and return changed files plus risks.
- The main agent reviews/merges the returned work, then calls
tdd_loop_reportfor exactly one completed ready task. - The tdd_loop extension runs that task's authoritative verifier plus configured validation and decides whether the task is complete.
Optional: set "taskWorktrees": true on a dag-plan spec to have the extension create one git worktree per ready task. The task prompt names the worktree path and branch. The agent works there, the extension verifies and validates there, commits reported artifacts on the task branch, merges the task commit into the orchestrator worktree, and reruns the task verifier plus validation in the orchestrator worktree before marking the task done.
Do not let subagents decide loop completion. Do not run parallel implementation agents against the same files, shared schema, migrations, fixtures, or domain service unless the DAG serializes those steps. If several delegated tasks return at once, report them one at a time so each verifier/commit boundary stays auditable.
A delegated DAG step prompt should include this contract:
You own only this TDD loop task.
Allowed file ownership: <paths/globs>.
Write or update one behavior-focused failing test through the public interface.
Implement the smallest fix for this behavior only.
Do not refactor unrelated code or touch files outside ownership; stop and explain if required.
Run: <check>.
Return: changed files, command result, old-behavior failure rationale, deferred edge cases, and integration risks.Use low parallelism first, typically 2. Prefer parallel delegation for leaf UI/tests/adapters/docs; serialize core domain services, schema changes, migrations, shared types, and fixtures.
Run memory is written by default to:
.pi/loops/runs/<loop-name>/Each completed step creates a local git commit for the file paths the agent reports in tdd_loop_report.artifacts. Failed iterations are not committed. Set "autoCommitNoVerify": true in a loop spec to bypass hooks for that loop.
With "taskWorktrees": true, task worktrees are created as siblings of the orchestrator repository using names like <repo>.tdd-<run-id>-<task-id> and branches like tdd-loop/<name>/<run-id>/<task-id>. If merge or post-merge verification fails, the task is retried and is not marked complete.
If Pi is interrupted or restarted mid-loop, the run state is saved to .pi/loops/runs/<name>/. Run the same loop command to pick up the latest unfinished run:
/tdd_loop fix-login-bugUse /tdd_loop fresh fix-login-bug when you intentionally want a new run from the spec.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | Unique spec name, used for file naming |
goal |
string | "Complete the requested task." |
Description of what the loop should achieve |
prompt |
string | — | Instruction for each iteration. Legacy alias: taskPrompt. |
check |
string | "npm test" |
Shell command that proves completion. Legacy alias: verifyCommand. |
attempts |
number | 5 (tdd), 3 (plan) | Simplified attempt count. For single-step loops this is max total attempts; for plans this is attempts per step. |
maxIterations |
number | 5 (tdd), computed (plan) | Advanced: max total iterations, capped at 50 |
maxIterationsPerStep |
number | 3 | Advanced legacy name for per-step attempts |
timeoutMs |
number | 120000 | Timeout per check run. Legacy alias: verifyTimeoutMs. |
steps |
array | [] |
Simplified alias for plan steps. Any dependsOn infers dag-plan; otherwise ordered test-plan. |
plan |
array | [] |
Legacy name for steps |
finalCheck |
string | — | Full-suite check after all plan steps pass. Legacy alias: finalVerifyCommand. |
validationCommand |
string | fallow audit --format json --quiet --fail-on-issues |
Validation command run after a verifier exits 0; failure output is fed back into the next task iteration for mitigation. Set to an empty string to skip validation. |
parallelism |
number | 1 | How many ready DAG tasks to surface (does not auto-parallelize) |
taskWorktrees |
boolean | false |
For dag-plan, create per-task git worktrees, commit task branches, merge into the orchestrator worktree, then rerun the task verifier plus validation before completion |
autoCommitNoVerify |
boolean | false |
Use git commit --no-verify (bypasses pre-commit hooks) |
autoCommitAddUntracked |
boolean | false |
Allow committing untracked/new reported files via a last-moment git add -N. Off by default for safety. |
verifyOutputPattern |
string | — | Regex pattern that must match verifier output (stdout+stderr) for exit 0 to count as "done". Prevents vacuous passes from empty test suites. |
verifyPrefix |
string | — | Required prefix for verifier check commands (e.g., "npm test"). Validation commands are exempt so the default Fallow pass can still run. Commands not starting with this prefix are rejected at startup. Lightweight sandbox for shared specs. |
dependsOnfor DAG tasks that must wait for other tests to pass.- DAG cycle detection: loops with dependency cycles are rejected at startup with a clear error message.
- Vacuous pass guard: set
verifyOutputPatternto a regex that must match verifier output. Exit 0 without matching output counts as failure — prevents empty test suites from passing. - Dirty-tree warning: warns before starting if the working tree has pre-existing uncommitted changes; auto-commit only commits paths reported in
tdd_loop_report.artifacts. - Abort safety: if the verifier is cancelled mid-run, loop state is preserved without committing or advancing.
maxIterationscapped internally at 50.attemptsfor test/DAG plans.- Every completed step creates a local commit when reported artifact paths have changes. By default pre-commit hooks are respected; set
autoCommitNoVerify: trueto bypass them. - Task worktree isolation:
taskWorktrees: truefor DAG plans runs each task in its own branch/worktree and requires merge plus post-merge verifier and Fallow validation success before marking the task complete. - History truncation: prompts show the last 10 iterations to keep token usage bounded for long runs.
- Duplicate task guard: reporting an already-completed DAG task ID is safely rejected.
- Final verifier cap: the final verifier (
finalCheck) uses a dedicated attempt counter, separate from the step iteration budget. - Atomic writes: all state files are written via temp file + rename to prevent corruption from interrupted writes.
- Command prefix guard: set
verifyPrefixto lock verifiercheckcommands to a specific tool (e.g.,"npm test"). Validation commands are exempt. Rejected at startup if a verifier command doesn't match. - Dangerous command blocklist: commands like
rm -rf,dd,mkfs,sudoare blocked by default insrc/command-policy.ts. Removable from source if intentionally needed. - TOCTOU race prevention: the run slot is claimed synchronously before any async operation, preventing concurrent
/tdd_loopstarts from racing. - Reported paths only: auto-commit uses
git commit --only -- <reported paths>so unrelated dirty or staged changes are not swept into the step commit. - Last-moment index use: tracked-file commits do not run
git add; ifautoCommitAddUntracked: true, the loop runsgit add -N -- <reported paths>immediately beforegit commit --onlyso new files can be included with minimal index interference. - Unknown field detection: misspelled or unknown fields in spec JSON are detected at parse time with a clear
console.warnmessage, preventing silent configurability errors.
The repository-local .fallowrc.json is only for validating this extension repository. It sets the extension entry point plus behavior tests as Fallow entry points and currently ignores index.ts for duplication/health because the extension still contains pre-existing single-file complexity. The default loop validationCommand runs against the user's active project and uses that project's Fallow configuration, not this extension-local config.
For fuzzy goals, first create a test plan or add a checker/approval gate, for example review score >= 8/10 or human approved.