Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tdd_loop

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.

Install

Clone into Pi's global extension directory:

git clone https://github.com/k1000/tdd_loop.git ~/.pi/agent/extensions/tdd_loop

Then restart Pi or run:

/reload

Commands

/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 spec

The Pi UI widget shows live status while a loop runs.

Skill

The package includes a skill:

/skill:tdd_loop

Use it to design deterministic loop specs and turn expected coding functionality into test-based completion criteria.

Choose the shape

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

Single-test spec

{
  "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
}

Plan spec

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.

Deterministic runtime protocol

/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 => blocked

In 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.

Delegated DAG protocol

Delegation is useful for dag-plan loops, but the loop verifier remains the source of truth.

Recommended pattern:

  1. The main agent decomposes the goal into DAG steps with explicit dependsOn, narrow checks, and expected file ownership in each prompt.
  2. The main agent may delegate ready DAG tasks to subagents only when those tasks are independent and file ownership is disjoint.
  3. 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.
  4. The main agent reviews/merges the returned work, then calls tdd_loop_report for exactly one completed ready task.
  5. 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.

Resume a run

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-bug

Use /tdd_loop fresh fix-login-bug when you intentionally want a new run from the spec.

Spec fields

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.

Safety guarantees

  • dependsOn for 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 verifyOutputPattern to 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.
  • maxIterations capped internally at 50.
  • attempts for 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: true to bypass them.
  • Task worktree isolation: taskWorktrees: true for 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 verifyPrefix to lock verifier check commands 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, sudo are blocked by default in src/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_loop starts 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; if autoCommitAddUntracked: true, the loop runs git add -N -- <reported paths> immediately before git commit --only so 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.warn message, preventing silent configurability errors.

Extension self-check Fallow config

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages