Loop Engineering

A Better Way to Think, Create, and Work with AI - Companion site

← All worked examples

Worked Example - Working Memory and the Scaffolding Loop

Companion to Chapter 6. A scaffolding-loop trace for a real task: refactoring a payment-processing module across four files while a session limit looms. The example shows how NOTES.md protects state across the context-cliff that would otherwise force a restart.

The task

Refactor PaymentProcessor from a single 800-line class into four cohesive modules (input validation, gateway interaction, ledger write, notification dispatch) without breaking the 47 tests that cover the existing behavior.

The discipline

Three of the seven commitments carry the loop here:

Iteration 1 - frame and plan

NOTES.md after iteration 1

# PaymentProcessor refactor ## Goal Split PaymentProcessor (800 lines, 1 class) into 4 modules without breaking the 47 tests in tests/payment_processor.test.ts. ## Plan 1. InputValidator - extract validate*() methods (lines 40-180) 2. GatewayClient - extract gateway send/retry (lines 181-420) 3. LedgerWriter - extract recordTransaction*() (lines 421-620) 4. NotificationDispatcher - extract notify*() (lines 621-790) 5. PaymentProcessor stays as the orchestrator (calls the 4 modules) ## Test contract - Run `npm test -- payment_processor.test.ts` after each extraction - If a test fails, do NOT proceed to the next extraction - Capture the failing test name + error here ## Tried and abandoned (empty - start of work)

Iteration 2 - extract InputValidator

Extract validators to src/payment/InputValidator.ts; update PaymentProcessor to delegate. Run tests.

Test result

PASS tests/payment_processor.test.ts 47 passed, 0 failed

NOTES.md update

## Plan 1. [x] InputValidator - extracted; 47/47 passing 2. GatewayClient ... 3. LedgerWriter ... 4. NotificationDispatcher ... ## Tried and abandoned - Considered moving InputValidator into the constructor as a private field; tests pass but breaks the DI pattern the rest of the codebase uses. Reverted; passing as a constructor argument instead.

Iteration 3 - extract GatewayClient, hit a test failure

Test result

FAIL tests/payment_processor.test.ts 1 failing: "retries on 503 with exponential backoff" Expected: 3 retry attempts with delays [100ms, 200ms, 400ms] Actual: 3 attempts but delays [0ms, 0ms, 0ms] The setTimeout in GatewayClient.send() runs but the test mock of setTimeout was attached to PaymentProcessor's scope, not the new GatewayClient module.

NOTES.md update - failure captured

## Tried and abandoned - ... - Considered moving the setTimeout call back to PaymentProcessor and having GatewayClient return a promise + delay value. Rejected: pushes timing concern out of the module that owns it. - The fix: update the test fixture to mock setTimeout on the GatewayClient module, not PaymentProcessor. Test now passes. ## Plan 1. [x] InputValidator - extracted; 47/47 passing 2. [x] GatewayClient - extracted; 47/47 passing after test fixture update 3. LedgerWriter ... 4. NotificationDispatcher ...

Iteration 4 - context-cliff event

Session hits the working-memory limit. The harness runs /compact. Earlier conversation is summarized; only the most recent exchanges remain in the rolling window.

Without NOTES.md, the loop would lose state: the partner would not remember which modules were already extracted, what failure modes were tried, or that the GatewayClient test fixture had been updated. With NOTES.md, the next iteration reads the file and resumes:

# Reading NOTES.md... # State recovered: # - InputValidator: done, passing # - GatewayClient: done, passing (with test-fixture lesson) # - LedgerWriter: pending # - NotificationDispatcher: pending # Next action: extract LedgerWriter (recordTransaction* methods, lines 421-620)

The context cliff is invisible to the loop's progress because the loop's state was never in the context to begin with - it was in the file.

Iterations 5–6 - finish and verify

LedgerWriter extracts cleanly (47/47). NotificationDispatcher extracts cleanly (47/47). Final PaymentProcessor.ts is 180 lines (the orchestration); the four extracted modules sum to 620 lines (down from the original 800 due to deduplication).

Architectural takeaway

The scaffolding loop did not make the partner smarter. It made the partner's state survivable. The same task without NOTES.md would have hit the context cliff and started losing track of what was done. With it, the loop ran for as long as the task needed.

Three commitments carried this trace; the other four (maker/checker, materiality gating, frame before solve, visible verification) are present but less load-bearing in this particular example. See the seven-commitments deep treatment for the failure mode of each when absent.

Practitioner templates

The working-memory prompts page hosts the NOTES.md template used in this trace, plus the scaffolding-loop contract and the verifiable-stop checklist.