Speed Buys Exploration

Speed is the axis everyone thinks they have mastered. Most teams have only pocketed it.

Every Indian child knows the story of the Samudra Manthan, the churning of the ocean for amrutha (divine elexir).

The devas (gods) and the asuras (demons), weakened and rivalrous, agree to churn the ocean together for amrutha, the nectar of immortality. Mount Mandara becomes the churning rod. Vāsuki, the serpent king, becomes the rope. When the mountain begins to sink into the ocean, Vishnu takes the form of Kūrma, the great tortoise, and holds it up from below.

The first thing the ocean gives them is not nectar. It is hālāhala, a poison so potent it threatens every world. Shiva drinks it and holds it in his throat, which turns blue. Only after that do the treasures come: the wish-granting cow, the celestial horse, Lakshmi herself. Amṛta comes last, carried by Dhanvantari.

I keep returning to this story because it is the most accurate description I know of what happens when you explore at speed.

The wrong lesson from speed

Speed is usually described as “the same work in less time” and treated as the least interesting kind of ambition. For most office work that is fair. For code, I think dismissing it hides the most important shift.

Watch what most teams actually do with agentic speed. A story that took three days now takes one. The team ships it, picks up the next story, and reports a velocity improvement. The design process is unchanged. One engineer still picks one approach, defends it in a review, and builds it.

That is pocketing the speed, not using it.

When a spike cost a sprint, running one and defending it was rational. When a spike costs an afternoon, the rational move is to run three and throw two away. The economics of exploration have changed. Most habits have not.

Terence Tao has described the same shift in mathematics: because AI makes it cheap to test whether an idea fails immediately, he now tries ideas he would once have dismissed as too unlikely to be worth the hours. That is speed used properly. Not faster delivery of the same bet, but more bets.

Churn wide

Here are the patterns I now ask teams to use.

Competing implementations. For any design question that would otherwise take a meeting, have agents build two or three alternatives in separate git worktrees, independent working copies of the same repository, so each agent has its own branch on disk. Then measure. A forty-minute argument about whether to cache at the edge or in the service becomes a benchmark you can read in five minutes.

Kill early. Use a cheap prototype not to prove an idea works but to find out fast whether it fails. Most of the poison should surface in the first hour.

Characterise before you change. Before an agent touches a legacy module, have it write characterisation tests that pin the current behaviour, warts and all. This used to be a week of tedium nobody funded. Now it is an afternoon, and it changes what is safe to attempt.

Exhaustive refactors. Renames, dependency upgrades and API migrations across a whole codebase used to be deferred indefinitely. They are now cheap enough to just do, if the oracle is strong enough to catch regressions.

Someone must hold the poison

The manthan has three roles we tend to forget.

Kūrma, the stable base. Without the tortoise, the mountain sinks and the churning stops. In engineering, Kūrma is your leaders grit, your build, your test harness and your reproducible environments. Parallel exploration on a wobbly foundation produces noise, not options.

Shiva, who holds the hālāhala. Exploration produces bad output first, and in volume. Someone must absorb it without letting it spread: discarding failed branches, refusing to merge the clever but unverified variant, saying no. That is a review discipline, and it is a cost. Ignore it and the poison reaches production.

Mohini, who serves the amṛta. Selection is not the end of the churning. When the pot of nectar finally rose from the ocean, the devas and asuras fought over it, each side certain it was theirs. Vishnu took the form of Mohini and settled the fight not by argument but by controlling distribution. She seated everyone in rows and served the nectar herself, one row at a time.

This is the step most teams skip. The winning branch is the amṛta, but merging it is not the same as serving it. Mohini is progressive delivery: the change goes behind a feature flag to 1% of traffic, then 10%, then everyone. The argument about whether it really is the winner is settled by what real users experience, not by whoever argued best in review.

Rāhu, who slipped into the line. One asura, Rāhu, disguised himself as a deva and sat in the devas’ row. Before the Sun and the Moon recognised him, he had swallowed a sip. Vishnu’s discus severed his head, but the nectar had already passed his throat. The head lived on, and it still returns to swallow the Sun and the Moon at every eclipse.

Three lessons, in order.

  1. Disguise gets past selection. The branch that won the benchmark by caching stale data looks like a deva until it meets real traffic.
  2. You need watchers in the line. The Sun and Moon of your pipeline are canary analysis: comparing the cohort that received the change with the one that did not, on error rates, latency and the business metric that would reveal a disguise.
  3. The discus is not enough. Rollback cuts the change off, but whatever it already swallowed stays immortal: rows written, messages sent, caches filled, schemas migrated. So serve only what you can take back. Use expand-and-contract migrations (add the new schema beside the old, switch over, remove the old later), idempotent writes, and flags that turn the new path off without a redeploy. If Rāhu’s head survives, make sure it has nothing to eclipse.

Explore wide, merge narrow

The cost model has inverted. Tokens are cheap. Review attention is not. So the rule becomes: explore as widely as the oracle can judge, merge only what the oracle has judged, and serve it the way Mohini did, one row at a time.

This is also where I warned, in an earlier post on “-maxxing” anti-patterns, about loopmaxxing: running agents in ever-longer loops because you can. Parallelism without a selection mechanism is just loopmaxxing in several directions at once. Churn without selection and staged serving ends where the story’s churn did: in a fight over the pot.

For the Weekend

Pick the next design question your team would normally argue about. Instead of a meeting, write down how you would measure the answer. Then have agents build the two or three leading options and run the measurement.

Example. Suppose the team is split on how to cache product prices. One camp wants Redis, an in-memory data store beside the service. The other wants caching at the CDN edge, the content delivery network’s servers close to users. Instead of a meeting, run the churn.

Step 1: write the measurement before any code. This is the selection mechanism, Mohini’s job, and it is agreed first:

USER STORY
As the tech lead, I want an agreed, automated way to compare price-caching
designs, so that the team decides on evidence instead of opinion.
DEFINITION OF READY
- A staging environment with production-like traffic replay is available.
- An admin API to change a price (admin.set_price) exists.
DEFINITION OF DONE
- bench/README.md defines four measures: p95 latency at 500 requests a
second for 5 minutes; staleness (seconds from a price change until
every reader sees it); estimated monthly cost at current traffic;
behaviour when the cache layer is down.
- bench/load.js (k6) and bench/staleness.py implement the first two.
- bench/run.sh runs both against a BASE_URL and prints raw results.
- You MUST NOT implement any cache.
DEFINITION OF SUCCESS
- Two runs of bench/run.sh against current main agree within 10%,
proving the measurement itself is stable.

Step 2: two worktrees, two agents. A worktree is an extra working copy of the same repository on its own branch:

git worktree add ../prices-redis -b spike/prices-redis
git worktree add ../prices-edge -b spike/prices-edge

Each agent gets the same story, with the cache-specific lines changed:

USER STORY
As the tech lead, I want a working Redis price cache in this worktree,
so that it can be measured against the CDN option.
[CDN agent: "a working CDN edge price cache"]
DEFINITION OF READY
- bench/ is on main, and bench/run.sh works against staging.
- This worktree is on branch spike/prices-redis.
[CDN agent: spike/prices-edge]
DEFINITION OF DONE
- A Redis cache for GET /prices/{sku}, with a 30-second TTL (time to
live) and explicit invalidation on a price change.
[CDN agent: Cache-Control: s-maxage=30, plus a purge call on change]
- The cache sits behind the PRICE_CACHE_MODE setting, default off.
- You MUST NOT edit anything under bench/.
DEFINITION OF SUCCESS
- bench/run.sh completes, and its raw output is pasted into your report
unedited.

Step 3: the shared oracle. The load test uses k6, a load-testing tool scripted in JavaScript:

import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
steady: {
executor: 'constant-arrival-rate',
rate: 500, timeUnit: '1s', duration: '5m',
preAllocatedVUs: 200,
},
},
thresholds: { http_req_duration: ['p(95)<120'] },
};
export default function () {
const sku = `sku-${Math.floor(Math.random() * 5000)}`;
const res = http.get(`${__ENV.BASE_URL}/prices/${sku}`);
check(res, { 'status is 200': (r) => r.status === 200 });
}

And the staleness probe:

import time, requests
def staleness_seconds(base_url, admin, sku, new_price, timeout=120):
admin.set_price(sku, new_price)
start = time.monotonic()
while time.monotonic() - start < timeout:
if requests.get(f"{base_url}/prices/{sku}").json()["price"] == new_price:
return time.monotonic() - start
time.sleep(0.25)
raise AssertionError(f"{sku} still stale after {timeout}s")

Step 4: let the numbers choose. An illustrative result:

Optionp95 latencyWorst stalenessCache layer down
Redis38 ms0.8 sFalls back to the database; p95 rises to 210 ms
CDN edge22 ms31 s (purge is asynchronous)Requests go to origin; no errors

Now the real question is visible. It was never Redis versus CDN. It is whether a customer may ever see a price that is 30 seconds out of date. That is a product decision, and it takes five minutes once the numbers are on the table. The losing worktree is deleted with git worktree remove ../prices-edge. Shiva holds the poison; nothing reaches main.

Step 5: serve the amṛta. Suppose the product decision is that 30 seconds of staleness is unacceptable, so Redis wins. Merging the branch is not the end of it:

USER STORY
As the tech lead, I want the Redis price cache rolled out in stages, so
that if it misbehaves under real traffic, only a small cohort is affected
and we can take it back.
DEFINITION OF READY
- spike/prices-redis is merged behind PRICE_CACHE_MODE, default off.
- Dashboards show price-endpoint error rate, p95 latency and "price
mismatch at checkout" events, split by flag cohort.
- Turning the flag off restores the old path with nothing to clean up:
the cache only reads, and writes nothing the old path depends on.
DEFINITION OF DONE
- Flag enabled for 1% of SKUs, then 10%, 50% and 100%, with at least
24 hours at each stage.
- Before each step up, compare the cohort with the flag on against the
cohort with it off.
- Automatic rollback: if checkout price mismatches in the on-cohort
exceed the off-cohort's by more than 0.1 percentage points, or p95
latency exceeds 120 ms for 10 minutes, turn the flag off and open an
incident.
- You MUST NOT change the rollback thresholds.
DEFINITION OF SUCCESS
- 100% rollout reached with no automatic rollback, and checkout price
mismatches no higher than before the change.

The checkout-mismatch metric is the Sun and the Moon. It is the one place a stale price gives itself away, however well it did in the benchmark. And the DoR line about nothing to clean up is the answer to Rāhu’s head: this rollout can be fully taken back.

If you cannot say how you would measure it, you have discovered something more valuable than the answer: the oracle you are missing.

Beyond the Spec

Spec-driven development fixed vibe coding. It has also become the ceiling. An old Indian question about knowledge explains why.

In the Nyaya tradition, a student who made a claim was asked one question before anything else: kena pramaṇena? By what means do you know this? Where is the proof?

The question was not rude. It was the start of every serious conversation. Gautama’s Nyāya Sūtra, written nearly two thousand years ago, lists four valid means of knowing, the pramāṇas:

  • Pratyakṣa, perception. You saw it.
  • Anumāna, inference. You reasoned to it from a reliable rule, the way smoke on a hill tells you there is fire.
  • Upamāna, comparison. You recognised it by its likeness to something you already knew.
  • Śabda, testimony. A trustworthy person told you.

I have been thinking about this list a great deal, because I now spend much of my week reviewing artefacts that no human wrote. And I have noticed that most of our agentic coding practice runs on only one of the four.

What spec-driven development got right

Let me be fair to where we are. A year ago, much AI-assisted coding was a slot machine. You typed a wish, pulled the lever and hoped. Reviews were archaeology.

Spec-driven development changed that. Plan mode before code, where the agent proposes a plan for approval before it writes anything. A story small enough to fit in one branch. A CLAUDE.md or Copilot instructions file that tells the agent the house rules. A written specification the agent must satisfy and the reviewer can check against. I have pushed teams hard towards this. It made agent work legible.

But look at it through Gautama’s eyes. A spec is śabda. It is testimony: a trusted person telling the agent what is wanted. And when the agent finishes and reports “all tests pass, the feature is complete”, that is śabda too, from a source whose reliability we have not established.

Nyāya was strict about testimony. Śabda is valid only when it comes from an āpta, a reliable person who knows the matter and has no reason to mislead. An agent under pressure to finish is not an āpta. It is a very fast, very capable colleague who will sometimes tell you what you wanted to hear.

So we have built an engineering practice where the input is testimony and the output is testimony, and the only perception in the loop is a tired reviewer reading a diff at six in the evening.

The spec is a ticket with manners

Here is the limit in plain terms. A spec tells the agent what to build. It does not say:

  • what must remain true after the change, and keep remaining true;
  • what the best practitioner in this area would measure before believing the result;
  • how anyone, human or machine, would know the work is right without reading every line.

The agent can satisfy the spec and miss the point. It can pass the tests it was shown and break the property nobody wrote down. And because generation is now so cheap, it can do this at a volume no review queue can absorb.

A useful way to think about ambition with AI models splits it four ways: speed (same work, less time), altitude (aim at the outcome, not the task), depth (work the way the best expert in the field would) and range (bring in methods from other fields). It is a good map, and this series borrows it.

But for code, I think it needs a fifth axis, and that axis sets the ceiling on the other four.

The fifth axis: the oracle

In testing, an oracle is whatever tells you whether an output is correct. A unit test is an oracle. So is a type checker, a property-based test, a production SLO (service level objective, a target such as 99.9% of requests succeeding), a second reviewer who never saw the implementation.

My thesis for this series is simple: in agentic coding, your ambition is capped by your oracle.

If you cannot verify it, you cannot safely ask for it. Ask an agent to rewrite a payment reconciliation engine and the only thing standing between you and an incident is the quality of whatever checks the result. Improve the checks, and the size of what you can safely ask for grows. Leave them as they are, and better prompts will not help you. You will simply generate unverifiable work faster.

This is where the pramāṇas earn their place. Each one maps to a family of oracles, and a mature team uses all four:

PramāṇaMeans of knowingIn agentic coding
PratyakṣaPerceptionRun it and observe: integration tests, production telemetry, traces, reproduction on the real platform
AnumānaInference from an invariable ruleInvariants and properties: types, contracts, property-based tests, formal proofs
UpamānaComparison with the knownDifferential tests against the old system, golden files, benchmarks against a baseline
ŚabdaReliable testimonySpecs, documentation, review comments, and the agent’s own report, trusted only as far as the source has earned

In plain terms: traces follow a single request across every service it touches; integration tests exercise several real components together; property-based tests state a rule that must hold for every input and let a tool generate thousands of cases to break it; contracts fix the agreed shape and behaviour at a boundary between services; formal proofs are machine-checked arguments that code meets its specification; differential tests run old and new code on the same inputs and flag any difference; golden files are saved, approved outputs that new results must match.

The four pramāṇas appear in turn, then everything except śabda dims.

The Nyāya example for inference is worth dwelling on. Smoke tells you there is fire because of vyāpti, an invariable concomitance: wherever there is smoke, there is fire. Much of good engineering is the search for vyāptis. Wherever a request is authorised, an audit record exists. Wherever a document is ingested, the resulting graph satisfies its shapes. State the vyāpti, check it mechanically, and you no longer need to trust anyone’s testimony about that property.

Why this matters now

Terence Tao, writing about AI-assisted mathematics, has described a widening gap between how fast proofs can now be generated, how fast they can be verified and how fast humans can understand them. Swap “proofs” for “pull requests” and you have described every engineering team I talk to.

The teams I see pulling ahead are not the ones with the cleverest prompts or the longest specs. They are the ones who have quietly moved their best engineers from writing code to building the instruments that judge code. They treat telemetry, test design and invariants as the product, and the agent as the labour.

For the Weekend

Take the last five tasks you gave an agent. For each, write two lines: what was the spec, and what was the oracle? Then ask kena pramāṇena? How did you actually know the result was right?

A note on how prompts are written in this series. Every agent prompt uses one structure, borrowed from agile practice. The user story says who wants what, and why. The Definition of Ready (DoR) lists what must exist before the agent starts. The Definition of Done (DoD) lists what it must deliver and what it must not do, written with RFC 2119 keywords (MUST, MUST NOT, SHOULD) so that obligations are unambiguous. The Definition of Success (DoS) says how we will know it worked, measured by something other than the agent’s own report.

That last section is the oracle. If you can fill in only the first three, you have written a spec.

Example. Suppose last week’s task was a retry fix on a payment-status poller. Here is the prompt as it was given:

USER STORY
As an operations analyst, I want the payment-status poller to retry
transient failures, so that payments are not marked "unknown" after a
single network blip.
DEFINITION OF READY
- src/payments/poller.py and its unit tests are on main and passing.
- Retry policy agreed: at most 3 attempts; back-off 200 ms, then 400 ms;
retry on 5xx responses and timeouts only.
DEFINITION OF DONE
- Plan mode: present the plan before writing any code.
- Retries implemented in StatusPoller as per the policy above.
- The public interface of StatusPoller MUST NOT change.
- Unit tests added for the retry behaviour.
DEFINITION OF SUCCESS
- All tests pass.

The agent’s closing report said: “Implemented retries with exponential back-off. Added 2 unit tests. Timeouts and connection resets are handled. All tests pass.”

Now the two-line audit:

ClaimEvidencePramāṇa
Retries on 5xx, max three attemptsTwo unit tests using a mocked HTTP client (a fake stand-in for the real one)Weak pratyakṣa: observed, but against a fake
Handles timeouts and connection resetsThe agent’s sentenceŚabda only: nobody saw it work
Back-off doubles each timeNothingNone at all

Two of three claims rest on testimony. Now look back at the story. It was well formed, but its Definition of Success was “all tests pass”, and the agent wrote the tests. The next story fixes the DoS, not the code:

USER STORY
As the reviewer of the poller change, I want independent evidence for
each retry claim, so that I approve it on proof rather than on the
agent's report.
DEFINITION OF READY
- The retry change is on branch fix/poller-retries.
- tests/fakes.py provides ScriptedTransport and FakeClock.
- Hypothesis is installed in the dev environment.
DEFINITION OF DONE
- Property-based tests in tests/test_poller_properties.py, generating
outcome sequences drawn from {200, 500, 503, timeout, connection reset}.
- The tests MUST assert: (1) at most 3 calls; (2) if any of the first
three outcomes is 200, the final status is "ok"; (3) the waits are
200 ms, then 400 ms.
- Use FakeClock so the tests run instantly.
- You MUST NOT modify anything under src/.
- If a property fails, report the smallest failing sequence. You MUST NOT
fix it.
DEFINITION OF SUCCESS
- The properties run in CI on every pull request that touches
src/payments/.
- A Toxiproxy fault-injection run against the payments sandbox shows
correct retries on connection resets.

The resulting test looks like this:

from hypothesis import given, strategies as st
from payments.poller import StatusPoller
from tests.fakes import ScriptedTransport, FakeClock
OUTCOMES = st.sampled_from(["200", "500", "503", "timeout", "reset"])
@given(st.lists(OUTCOMES, min_size=1, max_size=8))
def test_retry_properties(sequence):
transport = ScriptedTransport(sequence) # one outcome per call; repeats the last when exhausted
clock = FakeClock()
poller = StatusPoller(transport, sleep=clock.sleep)
poller.poll("pay_123")
assert transport.calls <= 3
if "200" in sequence[:3]:
assert poller.last_status == "ok"
assert clock.waits == [0.2, 0.4][: transport.calls - 1]

Then pratyakṣa against something real. Toxiproxy is a small proxy that injects network faults between your service and a dependency:

# Drop the connection 500 ms into every response from the payments sandbox
toxiproxy-cli toxic add -t reset_peer -a timeout=500 payments_sandbox
pytest tests/integration/test_poller_faults.py -v

In this scenario, Hypothesis finds the gap within seconds: on a connection reset, the poller raises instead of retrying, because the agent caught TimeoutError but not ConnectionResetError. The report said resets were handled. Kena pramāṇena? Now we know.

If most of your answers are śabda, you have found the ceiling. The rest of this series is about raising it.