open-weight/skills/outside-in-double-loop/SKILL.md
Use when implementing any feature that requires multiple collaborating modules, services, or classes. Use when a task involves dependencies that do not yet exist. Use alongside the tdd skill.
npx skillsauth add jon23d/skillz outside-in-double-loopInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Start from the user-facing surface. Stub everything behind it. Get the test green. Then build the stubs for real — one at a time, each with its own test-first cycle.
This skill governs implementation ordering. The tdd skill governs the red-green-refactor mechanics within each cycle. Both apply simultaneously.
Outer loop — the user-facing contract (endpoint, CLI command, UI interaction). Write a test for what the user sees. Every dependency behind it is a stub. Get this test green. The codebase is now in a passing state and the contract is locked.
Inner loop — one deferred dependency at a time. Pick the next item from the task queue. Write a test for it. Stub its dependencies. Get the test green. Repeat until the queue is empty.
Step 1 — Write the outer test.
The test describes user-facing behavior: given this request, expect this response. Every collaborator is a vi.fn() / mock. Do not create interface files, type files, or "vocabulary" files yet. Let the test drive what types you need. Define stub shapes inline in the test file.
Step 2 — Run the test. Watch it fail. The failure is typically "module not found" or "function not found." This is correct.
Step 3 — Write the minimum to make the outer test pass. Create the implementation file (e.g., the route handler). Create interfaces or types only as the implementation file demands them to compile. Do not pre-define interfaces for dependencies you haven't reached yet.
Step 4 — Run the test. Watch it pass. The outer loop is now green. Every dependency is a stub. The codebase is in a passing state.
Step 5 — Record deferred tasks. For every dependency you stubbed, add an entry to the task queue. Write the queue as a comment block at the top of the file you are currently working in, or in your task tracking tool:
// Task queue:
// - [ ] TenantService (stubbed in acceptance test)
// - [ ] CustomerService (stubbed in acceptance test)
// - [ ] TaxService (stubbed in acceptance test)
// - [ ] InvoiceRepository (stubbed in acceptance test)
// - [ ] AuditService (stubbed in acceptance test)
Step 6 — Pop the queue. Start an inner loop. Pick the next dependency. Write a test for it. If it has dependencies that don't exist, stub them and add new entries to the queue. Get the test green. Mark the item done. Repeat.
Step 7 — Stop when the queue is empty. Do not add work that was not driven by a stub. If no test stubs a dependency, that dependency does not need to exist.
Do not pre-define interfaces for dependencies you have not yet reached.
Interfaces emerge from the test that needs them. When the outer test needs a UserService stub, define the stub shape inline. When you write the implementation file and need a typed parameter, then extract the interface — not before.
Do not build a dependency before its consumer's test is green.
When writing TaxService and you realize it needs a TaxRateProvider, do not go build TaxRateProvider. Stub it. Get TaxService green. Add TaxRateProvider to the queue. Build it later.
Maintain an explicit task queue. Do not rely on memory. Write it down. Cross items off as you complete them.
Do not expand scope beyond what stubs require. If the outer test passes with stubs and no inner implementation is needed for the task to be complete, stop. Do not write integration tests, real implementations, or "nice-to-haves" unless the task explicitly requires them.
One file at a time. Finish before switching. Do not open a new file until the current file's test is green. If you discover a new dependency while implementing, stub it, add it to the queue, and keep working on the current file.
You are building InvoiceRepository. Its test stubs SequenceGenerator:
// invoice-repository.test.ts
const sequenceGenerator = { next: vi.fn().mockResolvedValue('INV-00001') }
const repo = new InMemoryInvoiceRepository(sequenceGenerator)
InvoiceRepository test goes green. Now update the queue:
// Task queue:
// - [x] TenantService
// - [x] CustomerService
// - [x] TaxService
// - [x] InvoiceRepository
// - [ ] SequenceGenerator <-- added when InvoiceRepository stubbed it
// - [ ] AuditService
Pop SequenceGenerator. Write its test. It has no dependencies — no stubs needed. Get it green. Mark done. Continue.
development
Use when adding or modifying environment variable handling in TypeScript projects or monorepos — especially when using process.env directly, missing startup validation, sharing env schemas across packages, or encountering "undefined is not a string" errors at runtime from missing env vars.
testing
Use when creating a new skill, editing an existing skill, writing a SKILL.md, or verifying a skill works before deployment.
development
React UI design principles and conventions. Load when building or modifying any user interface or React components. Covers application type detection, visual standards, component design and structure, Mantine (business apps) and Tailwind (consumer apps), accessibility, responsiveness, state management, data fetching, testing, and in-app help patterns.
development
Use when setting up ESLint and/or Prettier in a TypeScript project, adding linting to an existing TypeScript codebase, or configuring typescript-eslint, eslint-config-prettier, or related packages.