Letting a coding agent free-run on "add rate limiting" produces a plausible diff that quietly picks a strategy, a store, and failure semantics for you.
Tier: Core · Workflow area: Feature development
The professional pattern is spec-first: you write a short spec, the agent proposes a plan, you review and correct the plan, and only then does it touch code — with you steering at checkpoints instead of auditing a 900-line surprise diff.
Skip it for one-file changes you could write faster than the spec.
Write the spec as constraints, not vibes. Example for an Express API:
SPEC: per-API-key rate limiting for /api/* routes.
- Sliding window, 100 req/min per key; Redis-backed (ioredis is already
a dependency); key = `rl:{apiKey}:{minuteBucket}`.
- On limit: 429 with Retry-After header and JSON error body matching
our errorResponse() helper in src/http/errors.ts.
- Redis unavailable => fail OPEN (allow request, log warn). Never 500.
- Exempt: /api/health, internal service keys (isInternal flag).
- Middleware lives in src/middleware/, wired in src/app.ts.
- Tests: unit-test window math with a fake clock; integration test the
429 path and the fail-open path.
Non-goals: per-route limits, dashboards, config UI.
Ask for a plan, not code. In plan mode (or prefixed with "Do not edit files yet"):
Read the spec and the relevant code. Produce an implementation plan:
ordered steps, files touched per step, and the test you will add per
step. Flag anything in the spec that conflicts with existing code.
Review the plan like a design doc. Check: did it find errorResponse()? Did it notice the existing auth middleware exposes the API key? Is fail-open actually handled or hand-waved? Correct the plan in chat — plan edits cost one sentence; code edits cost a review cycle.
Implement step-by-step with checkpoints. Approve implementation one or two steps at a time. After each step, make the agent run the tests and show output. If your tool supports it, keep edits gated on your approval.
Intervene early and loudly. The moment a diff drifts from the spec (different key scheme, swallowed Redis errors), stop, restate the constraint, and have it redo that step. Steering beats salvaging.
Final pass: diff-vs-spec review. Read the full diff against the spec line by line, then ask: List every spec requirement and where in the diff it is satisfied. Mark any requirement you did not implement. Unimplemented items become your punch list.
The spec is the contract and the plan review is the negotiation — by the time code exists, there should be nothing left to argue about except details.
SPEC.md) and reference it in every instruction; use checkpoint/review UI between steps.git stash check) — proving they test the feature, not themselves.