Kashmir

It’s Tvisha’s summer vacation, and due to work pressures, we could not take her out of Bangalore in April. So she was bugging us to get out of Bangalore. After comparing Leh/Ladakh, Sikkim, Andamans, and Kashmir, we finally settled on Kashmir. I had been to Kashmir when I was very young and had fond memories of the place. I was unsure whether it was safe to visit Kashmir, and after much online research and talking to the travel agents, we zeroed in on Kashmir.

Some friends were going on a Bangalore-Leh drive (9K+ Kms), and others were going to monasteries in Sikkim. We had recently been to monasteries in Bhutan, and the girls were not in the mood for another long drive! In December 2021, we enjoyed a seven-day long drive (Bangalore-Coonoor-Kotagiri-Coimbatore-Mahabalipuram-Bangalore). The weatherman predicted rains in Andamans, and the daughter wanted to make Olaf! So, Kashmir was inevitable!

After comparing rates and dates with our favorite travel agent (SOTC), we finally booked our trip with the MakeMyTrip holidays.


Day 1 – Bangalore to Srinagar

There is a direct Indigo Flight (6E 797) from Bangalore to Srinagar that starts in Bangalore at a convenient time (~9:15 AM) and reaches Srinagar post noon (~2:00 PM) with a brief halt at Amritsar.

When the plane landed at the Srinagar airport, all passengers were in awe of Srinagar’s beautiful hills (some capped with snow). At the airport, our driver greeted us, and he instantly realized that we South Indians were in Kashmir to see snow in summer! He promised us ample snow in Sonmarg and Gulmarg, making Tvisha very happy!

It was a short drive from the airport to our first hotel in Srinagar. It was not a classic hotel but a houseboat. The houseboat we stayed in was Naaz Kashmir (https://www.naazkashmir.com/) and was located in Nageen Lake (and not Dal Lake). All the lakes in Srinagar are connected, but Nageen Lake is less commercial and crowded.

The rest of the day was free for us to enjoy the houseboat! We spent our time taking photos, watching fishermen and birds catch fish, eating pakoras, listening to the music of the water and birds, listening to the prayers from different surrounding Mosques, dressing up in a traditional dress, and a short shikara ride.

It was an abrupt stop to the fast life we are used to, staring back at nature and adoring its beauty. We talked to each other as a family and were not immersed in our gadgets! However, Tvisha was excited to show off her first day in Kashmir to her friends in a WhatsApp call!

Naaz Kashmir served us well – candlelight dinners, food per our needs, and recommendations about Shikara rides. So we took their advice and decided to take the 4:30 AM four hours Shikara ride from Nageen Lake to Dal Lake.

Staying in a houseboat is like sharing a house with other equally clueless and excited families.


Day 2 – Srinagar (Shikara Ride)

We kept the alarm to wake us up at 4:00 AM to prepare for our 4:30 AM shikara ride. We woke up to the alarm and morning prayers at the lake. It reminded me of my pre-college days when 4:00 AM had become a routine for studies. The caretakers were already up and knocked on our doors to ensure that we were ready and sent us on the shikara with some hot Kashmiri tea (Kahwa) and snacks.

It was bitter cold (for us) and pleasant for the locals. We tugged ourselves into the blanket available in the shikara. Our little braveheart sandwiched herself conveniently between her parents, refused to step out of the blanket throughout the ride, enjoyed the frequent warm rubs, and did not hesitate to nap. The shikara moved slowly, confidently, and thoughtfully through the lake.

The cold air on the face and the calming sound of the shikara moving in the lake is an unforgettable experience. Most of the locals and birds were up at 4:30 AM!! As we rowed through the lake, we could see the homes of the locals – men and women at work. Everyone that met the eye returned a welcoming smile.

We spotted a water snake, various colored water lilies, and birds like eagles, geese, mallards, pochards, gadwalls, pintails, waders, coots, and the common teal. The shikara rider helped us identify the birds.

We could see water vapor causing fog in some places due to the difference in water temperatures and the surroundings.

The shikara rider explained to us in detail the unique farming done by the locals in the lake, i.e., the process in which they grow vegetables. These farmers grow carrots, radishes, turnips, and other vegetables in the soil that floats on flora beds.

Such floating gardens are maintained all over the lake, and the farmers move through their plots on boats carrying their harvest to the lake’s market area. The rider took us through the floating gardens and the lotus farms to the floating vegetable market on dal lake. We bought some flowers and seeds and had some hot Kahwa (Kashmiri Tea) and snacks packed for us by the Naaz Kashmir at this market.

The rider then took us to the dal lake (pronounced as दल and not दाल), and at this time of the day, it was empty except for the locals fishing out the water weeds to compost them for use in their farms. It was a beautiful sunrise to watch at the dal lake, wade through the markets (Meena Bazaar), and spot traditional homes. The ride back was slow, and the gentle swaying motion of the shikara ride put us to sleep for a few minutes.


Day 2 – Sonmarg (सोनमर्ग and not सोनमIर्ग)

At around 9:30 AM, after breakfast, we started driving to Sonmarg, a hill station in the Ganderbal district.

It was about two hours drive. The roads were not too good, and there were traffic jams in a few places. If we had left about an hour earlier, we might have saved about 1/2 hour of jam time. It was visitor traffic. However, after the early morning Shikara ride, we needed some time to fuel and freshen ourselves before our next adventure.

As we reached closer to Sonmarg, we could see the snow-capped mountains, feel the drop in temperature, and breathe the superior quality air. Sonmarg was much cooler than Srinagar but fortunately pleasant (even for our visitor skin).

During the drive, we could see streams of water – the tributaries of Jhelum – Lidder, Sind, and Neelum. The sight of white water forcing itself down the mountains was strangely peaceful. It reminded us of our stay in Salt Lake City, Utah.

The driver told us that during the winter months, heavy snowfall blocks NH-1. So a tunnel is being built to keep the road open year-round. We found trekkers ready to trek from Sonmarg to Leh (“the rooftop of the world”) and groups wanting to drive through the Zoji La pass. Our driver informed us that driving through the Zoji La pass is a must-do off-road adventure. However, he recommended we not take our city car and take a four-wheel drive as the roads are narrow and rocky. It seems it’s a trek worth doing! So, now this one is also added to the backlog!

However, our goal for today was relatively simple. We either trek or take ponies to the Thajiwas glacier. This glacier is a favorite summer destination at Sonmarg.

My daughter is always excited and happy around animals. We were not sure whether we (“The Adults”) needed ponies; however, the agents there convinced us that the pony ride is a “must-do” in Sonmarg. We gave in; however, it would have been a simple to moderate walk/trek and less burden on the animals in hindsight. The ponies were expensive, about 2750 INR / person (we negotiated out more than 50% of the original demand. Better negotiators got it at around 1500 INR / person). We also got a photographer for us to be more hands-free. The pony ride was uphill, downhill, and through cold water streams.

The scenery was picture perfect, a silvery scene set against green meadows and a clear blue sky. The views were captivating, and we outsourced the picture capture activity to our photographer and instead enjoyed the views. The air was too fresh to keep our masks on.

At the glacier, Tvisha finally made her Olaf! She was throwing snowballs at us in all directions, even when we were haggling with the locals to reduce the sledding costs! She was able to go up and down the snow and was soaking in the happiness, unlike us. Then, after an uphill trek in the snow, we sat down on a rock and came down sledding. The weather was not too cold, and if it were not for the time to return to Srinagar and limited food options, we would have spent more time up at the glacier.

We encountered the local police (to the surprise of the locals), who helped us reduce the cost of sledding from 3500/- to 500/- INR (though we ended up paying 1000 INR per person for sledding). It’s funny that the locals keep saying (क्या आप खुश है, हमे भी खुश कीजिये) “Are you happy? Make us happy!”, a method to get more money from the visitors. These people make money only during the summer months (visitor months) and try to make the most of it. COVID & CONFLICT closures have been hard on them. We found these people to be cheerful, happy, and helpful. So, we did not hesitate to give (tips) more than we thought was reasonable!

We stopped at a roadside restaurant to have some delicious chole-puri and dal-makhani for Tvisha on our way back. We reached back around 7:00 PM for another candlelight dinner at Naaz Kashmir. The owners of Naaz Kashmir had moved our luggage out of the room as they thought that we booked for only one night, and they realized that there was a communication issue between MakeMyTrip and their reservation team. They made up for it by giving us a superior room, a chocolate cake for Tvisha, and several apologies.

We took a warm bath and crashed soon after!

We were woken up by Tvisha mid-night as she started vomiting and was feeling very sick! She had no temperature but did not look well. We were not sure whether it was the altitude, change in food/water, or a stomach bug. Thankfully, we had carried medicines – “Enterogermina” and “Calpol” in our first aid kit!


Day 3 – Pahalgam

We started around 9:45 AM after thanking the caretakers for their service at Naaz Kashmir and completing the check-out formalities. Tvisha was sick and only managed to eat mangoes for breakfast. The drive to Pahalgam was long but on more convenient and motorable roads. We wanted to stop for Kahwa and visit apple farms, but Tvisha would only sleep in the car. So, we drove straight to our hotel, the Radisson Golf Resort.

The Pahalgam scenery was unique, with white water streams and a backdrop of pine trees and snow-capped mountains. The temperature was pleasant and leaning towards too cool.

All we could do on day-3 was check in and rest. Tvisha slept all afternoon and night. She was running a slight fever, so we consulted our family doctor, and she diagnosed her to have the stomach flu. So we requested the hotel to get us medicines (the medicine shop was ~2KMS far), and they helped us.

The trip managers advised us to visit Abu Valley, Betaab Valley, Chandanwari, and Baisaran valley (Mini-Switzerland). However, we had lost 1/2 a day and had to check out by 1:00 PM the next day (Day-4). So we decided that if Tvisha feels better on Day-4, we will do the Baisaran valley.

Pahalgam (First Village) gets its name from Hindi “पहला गांव” and Shiva devotees visit this place frequently in summer to pilgrimage to the Amarnath Cave for the Darshan of the only ice stalagmite Shiva Linga. The pilgrimage usually starts from Chandanwari but was closed due to road work during our travel.


Day 4 – Pahalgam

Tvisha woke up ready for her next adventure; however, I felt queasy in my stomach. It was my turn to fall sick. However, Dolo came to my rescue, and after an insignificant breakfast, we all jumped up on horses to visit the Baisaran valley. This trek is captivating, and it’s better to trek than take horses. However, a just recovered Tvisha and a queasy daddy were not in any state to trek. So, horses again!

Baisaran Valley is a hilltop green meadow dotted with dense pine forests and surrounded by snowcapped mountains. This famous offbeat tourist place is excellent for those wanting to spend a quiet time in the company of nature. It also serves as a campsite for trekkers going to Tulian Lake. Some of the famous tourist points you can see en route to Baisaran are Pahalgam Old Village, Kashmir Valley Point, and Deon Valley Point. You can also enjoy panoramic sights of Pahalgam town & Lidder Valley from here.

We returned to our hotel after about four hours (~1:00 PM) ride to Baisaran. We completed our checkout and had a delicious rainbow trout tandoori (a local delicacy) for lunch. The driver told us that butter or olive fry is better. After a sumptuous meal, we departed by car to Gulmarg.


Day 4 – Pahalgam to Gulmarg (गुलमर्ग )

It took about 3.5 hours from Pahalgam to reach Gulmarg. Gulmarg is a ski destination and is famous for winter sports. It is called the meadow of flowers in summers. Our driver informed us that they have to use chains on wheels in winter, and the snowfall in Gulmarg is heavy. The beauty of Gulmarg was different (in a good sense) than Pahalgam and Sonmarg. The first thing that strikes out is the lush green meadows (in summers).

We camped in the Hilltop Hotel. At first glance, the hotel seems jaded, faded, and under maintenance. While that is true, the rooms were well designed, and the in-room dining service was good. The service at this hotel was better than any of our previous experiences in Kashmir, even though they were ok at room cleaning services, breakfast variety, bathroom accessories, changing towels, or responding to your hails. We were satisfied that we could have a warm bath, eat something edible, and reach on time for the gondola ride the following day. The hotel is close to the gondola ride and ice skating rink.

Dolo carried me only this far, and I had slight chills and crashed for the night. I hoped, wished, and prayed that the fever would help kill the virus/bacteria (stomach bug) for me to enjoy Gulmarg the next day.


Day 5 – Gulmarg – Gondola Ride to Kongdoori

The chills were gone in the morning; however, I was still queasy. So, I trusted my best friend (Dolo) and braved the gondola.

MakeMyTrip was able to arrange gondola tickets for the first stage (Base Station to Kongdoori). The tour guide told us that the second stage tickets (Kongdoori to Apharwat) were unavailable (sold out). However, in hindsight, we did not regret not being able to do the second stage.

The gondola wait lines and wait times are infamous. People start queuing at 7:00 AM, even though the ride opens at 9:30 AM. We reached the queue at around 8:00 AM and found our tour guide. The people standing in the line entertained themselves by fighting with others who tried to move ahead. Words and punches were flying until the ride opened at 9:30 AM. The locals were also amused at the sight. The trip was about 10 minutes, and the wait time to board the ride was 2 hours. Dolo kept me on my feet.

The gondola ride is short, and the views are terrific. The valley is picturesque, and we can spot the snowcapped Himalayas, Apharwat, and Mud houses from the ride. We could also see the unlucky visitors (those who could not get a gondola ticket) trekking or using horses to climb up to Kongdoori. This trek is a moderate to challenging hike.

At Kongdoori, people had to queue again to ride to Apharwat, and the queue was equally long. We were happy that we don’t have to wait in another queue.

Gondola Ride to Kongdoori (Friend: Dolo)

At Kongdoori, the horse owners were bugging us to take a horse ride to the waterfall. However, we discussed it with our tour guide and decided to trek. Trekking (and not horse riding) was the best decision. We could stop to look at the multi-colored flowers in the meadows, jump over streams, trek with the goats, stop to hear the sound of silence, spot lizards, take photos, and experience rocky trails.

When we reached the waterfall, we had to trek in snow to get up to the mouth of the waterfalls. The snow trek was challenging.

However, the mountain water was delicious and pure. We drank from the waterfall, and this water tasted better than any mineral or filtered water. So we filled a bottle to quench our thirst for the return journey.

If I travel again to destinations like these, I will remind myself to buy shoes with some grip.

Finally, we sledded downhill and relished on some delicious maggie cooked in the mountains waters. Strangely, maggie gave us the strength to trek back to the gondola ride station.

We took a different route to see the valley and meadows from a different perspective. This route was shorter and required us to climb uphill and roll downhill.

Again, the view was picture perfect!

After reaching the base station, we rushed to feed ourselves a late lunch. The lunch was good. We did not have any energy left to do the ATV rides and decided to skip them and relax in the room. Then, in the evening, we went down to the cafeteria to have some snacks. Our legs could not tolerate any more walking and would only walk back in the direction of the hotel room. So, we snuggled back into the room and watched the evening walkers from the comfort of the room.

Days are long in Gulmarg, and it’s bright even at 7:00 P.M.

I recovered from the stomach bug (Thanks! Enterogermina), and now it was time for my wife to fall sick to the same bug! She had a better immune reaction to the bug than my daughter or me; however, she sought help from Dolo and Enterogermina to fight off the bug.

Day 6 – Back to Srinagar

Gulmarg is about ~50Kms from Srinagar. So, the return journey was short. We woke up late and lazy, and left for Srinagar after a late breakfast.

We stopped to see apple farms and drink delicious green apple juice. We tasted various homemade pickles and bought lotus stem pickles from the farmer. Lotus stem (कमल-ककड़ी), locally known as “Nadru” is grown in shallow parts of water bodies like ponds and lakes and is a vastly enjoyed ingredient in Kashmiri cuisine.

We also stopped to have some premium Kahwa and buy some dry fruits (Walnuts) and condiments (Kesar).

We checked into Radisson Srinagar, the first hotel in Kashmir, where we found women employees. My wife had a heart-to-heart talk about women empowerment with the ladies there!

After a good lunch, we headed to see the oldest temple in Kashmir, the Shankaracharya Temple, dedicated to Lord Shiva. The temple is a monument of national importance and is protected by ASI (Archeological Society of India). There are many steps to climb, and the view of Kashmir valley from the hilltop is superb. It was very windy up the hill and pleasant. Unfortunately, cameras were not allowed for a picture remembrance. After blessings from Lord Shiva, we decided to stroll the gardens of Srinagar.

Unfortunately, due to the holiday rush and this day being a Sunday (many locals were out sightseeing), we saw only the Botanical Garden and the Chashme Shahi. We missed the Tulips as the Tulip Garden was closed a few days back (Peak season: April). The botanical garden was a nice walk, and the flowers in Chashme Shahi were exquisite. My daughter enjoyed taking photos of several flowers.

We had earlier decided to ride the Shikara again at Dal Lake; however, we decided to dart back to the hotel, looking at the rush and weather. Finally, we finished the day with a lavish buffet dinner.

Day 7 – Back to Home @ Bangalore

The only eventful activity was the security checks at the Srinagar Airport. We must step out of our cars at least a kilometer before the airport and have to get ourselves, the car, and the bags checked.

We left Srinagar entirely mesmerized by the beauty of Kashmir, and I decided to pen this down in a blog (for us) so that this never fades from (our) memory. So this blog is my first travel blog.

After a few more doses of Enterogermina and home food, my wife got better. We have returned to our workaholic ways and keep discussing our Kashmir trip with friends and family.

One Task, Five Ways

A framework only matters if it changes the work. Here is one ordinary task, taken through every axis in the series.

When Rāma reached the ocean, he could not cross it. Hanumān could leap; an army could not. In Vālmīki’s telling, the ocean itself advised him: among the vānaras was Nala, son of Viśvakarmā, the divine architect, and Nala could build a bridge.

Nala did not do it alone, and he did not do it in one go. Vālmīki gives the progress day by day. Fourteen yojanas (a yojana is an ancient measure of several miles) on the first day. Twenty on the second. Twenty-one, twenty-two, and on the fifth day twenty-three, reaching the far shore. Later tellings add that the stones floated because Rāma’s name was written on them. I prefer Vālmīki’s version, with its daily measurements. It reads like a project log.

This last post is my project log. One task, five days, one axis per day.

The bridge grows day by day by Vālmīki’s figures (14, 20, 21, 22 and 23 yojanas), and each day is labelled with its axis.

The task

The task is deliberately ordinary. A knowledge-graph pipeline ingests service documents and turns them into facts. We need it to support a new document type: a structured fault report that arrives as semi-structured PDF. The pipeline already handles four other types.

Before the first stone: the spec-driven baseline

This is how most good teams work today, and it is not a bad way. Plan mode means the agent proposes a plan for approval before it writes any code.

USER STORY
As a field-service engineer, I want fault reports ingested into the
knowledge graph, so that I can query past faults for a component.
DEFINITION OF READY
- Three sample fault reports are in fixtures/fault_reports/.
- The field-to-ontology mapping is agreed in SPEC-142.
DEFINITION OF DONE
- Plan mode: present the plan before writing any code.
- A fault-report parser in parsers/, following the existing pattern.
- Fields mapped to the ontology as SPEC-142 describes.
- Unit tests for the three samples.
- You MUST NOT change other parsers.
DEFINITION OF SUCCESS
- All tests pass.

The agent plans, writes the parser and the tests, and the tests pass. A reviewer reads the diff and approves it. Total time: under two hours. It works on the three samples. That is all we know.

Day 1: speed, so explore

USER STORY
As the pipeline owner, I want three parsing approaches compared on real
documents, so that we choose by measured accuracy, not preference.
DEFINITION OF READY
- The full archive of real fault reports is available, read-only.
- 50 hand-labelled reports are in eval/.
DEFINITION OF DONE
- Three parsers in separate worktrees: rule-based extraction, layout-aware
extraction, and model-assisted extraction with schema-constrained
output.
- All three run over the full archive.
- You MUST NOT edit anything under eval/.
DEFINITION OF SUCCESS
- One table reporting, for all three parsers, per-field precision and
recall against eval/, plus cost and latency per document.

Three bets instead of one, and a measurement instead of a debate. The poison surfaces early: one approach breaks on scanned pages, another on multi-page tables. That is the churning from Part 2 doing its job.

Terms: a worktree is a separate working copy of the repository; schema-constrained output forces the model to return data in a fixed structure; precision is the share of extracted values that are correct, and recall is the share of true values that were found at all.

Day 2: altitude, so name the invariant

USER STORY
As a field-service engineer, I want every fact in the graph to be valid
and traceable to its source, so that I can trust an answer at a customer
site.
DEFINITION OF READY
- SHACL shapes for all five document types are in shapes/.
- Ingestion logs are readable, and any document can be replayed.
DEFINITION OF DONE
- A check, run on every ingestion, that validates the graph against the
SHACL shapes and confirms every fact links to a page and span.
- A failure blocks publication of that document's facts.
- The new fault-report parser satisfies the check.
- You MUST NOT edit anything under shapes/.
DEFINITION OF SUCCESS
- Zero SHACL violations and zero facts without provenance across a full
re-ingestion of all five document types.

The task has moved from karma to dharma (Part 3). The invariant now protects all five document types, not just the new one. The plumbing this needed: access to the ingestion logs, a way to replay any document, and the shape validator wired into CI.

Terms: SHACL is a W3C language for stating rules a knowledge graph must satisfy; provenance means each fact records exactly which page and passage it came from.

Day 3: depth, so break the seed

USER STORY
As the pipeline owner, I want the parser tested against how fault reports
really arrive, not how SPEC-142 says they should, so that real documents
do not break it.
DEFINITION OF READY
- 500 real reports are sampled from the archive.
- The existing parser for the closest document type is available.
DEFINITION OF DONE
- A description of the variation found: layouts, missing fields,
languages, scan quality.
- Property-based tests: for any valid report, every required field is
extracted or explicitly marked missing, and no fact is emitted without
provenance.
- A differential test comparing overlapping fields with the closest
existing parser.
DEFINITION OF SUCCESS
- All properties hold over the 500 samples, and every differential
mismatch is either fixed or explained in writing.

This is Uddālaka’s lesson (Part 4). The spec had described the format as it was meant to be. The raw archive shows the format as it actually arrives.

Terms: property-based tests state rules that must hold for every input and let a tool generate thousands of cases to break them; a differential test runs two implementations on the same input and flags any difference.

Day 4: range, so borrow methods

USER STORY
As the pipeline owner, I want the fault-report path examined with
reliability and data-engineering methods, so that failures and breaking
changes are caught before downstream teams feel them.
DEFINITION OF READY
- Architecture notes for the ingestion path.
- A list of the downstream consumers of fault-report facts.
DEFINITION OF DONE (two sub-agents, in separate contexts)
- Reliability: an FMEA table for the ingestion path (failure mode,
effect, detection, recovery), and a fault-injection test for each
high-severity row.
- Data engineering: a data contract for the emitted fields, and contract
tests that fail if a field's type, cardinality or meaning changes.
DEFINITION OF SUCCESS
- Each fault-injection test fails with recovery disabled and passes on
main. Each contract test fails against a deliberately broken field.

Methods, not personas (Part 5). Two artefacts nobody had asked for, both of which a specialist would recognise.

Terms: FMEA (failure mode and effects analysis) lists how each part can fail, the effect and how it is detected; a fault-injection test causes a failure on purpose to prove the system copes; a data contract fixes the shape and meaning of data handed to other teams, and contract tests enforce it.

Day 5: the oracle, so protect the checks

USER STORY
As the human approver, I want this week's checks protected and the change
reviewed independently, so that what we merge can be trusted.
DEFINITION OF READY
- SHACL shapes, property tests, contract tests and fault-injection tests
all pass on the branch.
DEFINITION OF DONE
- All four kinds of check moved into protected paths the coding agent
cannot modify; changes there require human approval.
- A separate review agent, given only the diff, the tests and SPEC-142,
produces the strongest case that the change is wrong.
DEFINITION OF SUCCESS
- Every objection from the review agent is either disproved by a passing
test or fixed before merge.

Rāma’s ring (Part 6). On the fifth day the bridge reaches the far shore, because now we can trust what we built.

Terms: a protected path is a folder the coding agent cannot change; any change there needs human approval.

The log

DayAxisWhat changedResult
0Spec-drivenParser and unit tests for 3 fixtures[Time; defects found later]
1Speed3 parsers measured on the real archive[Chosen approach; precision and recall]
2AltitudeSHACL conformance and provenance as an invariant for all types[Existing violations found in other parsers]
3DepthProperty and differential tests from 500 real documents[Variants found that the spec missed]
4RangeFMEA with fault-injection tests; data contract[High-severity failure modes found]
5OracleProtected checks; independent review agent[Issues raised by the review agent; review time]

What the bridge was for

Rāma did not build the bridge. Nala did, with the vānaras carrying the stones. Rāma’s part was to know why it mattered, to ask for something that seemed impossible, and to accept nothing less than a bridge that would hold an army.

That is roughly where I think engineers now stand. The agents carry the stones, faster and in greater numbers than we ever could. Our work is to know what the crossing is for, to ask boldly, and to build the instruments that tell us, with a pramāṇa and not just a promise, that the bridge will hold.

We started this series with a question from the Nyāya tradition: kena pramāṇena? By what means do you know? It turns out that in agentic coding, that question is not a brake on ambition. It is what makes ambition safe.

For the Weekend

Pick one ordinary task/goal from next sprint. Run it the spec-driven way first. Then give it five days (or five milestones), one axis each, and keep a log.

Example. Suppose next sprint’s task is “add CSV export to the audit-log screen” in an internal admin application.

Day 0, spec-driven baseline.

USER STORY
As a compliance officer, I want to download the audit log I am viewing as
a CSV, so that I can attach it to an investigation.
DEFINITION OF READY
- The audit-log screen and its filter API exist.
DEFINITION OF DONE
- Plan mode: present the plan before writing any code.
- A "Download CSV" button on the audit-log screen.
- GET /audit-log/export.csv returns the rows matching the current
filters, with the same columns as the table.
- Tests added.
DEFINITION OF SUCCESS
- All tests pass.

An agent ships it in an afternoon. Two tests pass. Log it and move on.

Day 1, speed: build the alternatives and measure.

USER STORY
As the tech lead, I want two export designs measured at production
scale, so that we choose one on evidence.
DEFINITION OF READY
- A staging copy with 2 million audit rows.
DEFINITION OF DONE
- In two worktrees: (a) streaming the CSV row by row from a database
cursor; (b) a background job that writes the file to object storage
and emails a link.
DEFINITION OF SUCCESS
- One table giving, for both designs: time to first byte, total time,
peak memory, and behaviour when the user closes the tab halfway.

Day 2, altitude: name the invariant. The real requirement is not “a CSV”. It is that the export always matches what the user saw:

import csv, io
from hypothesis import given, settings
from tests.strategies import audit_filters
@settings(max_examples=200)
@given(f=audit_filters()) # random combinations of user, action, date range, text search
def test_export_matches_screen(client, f):
screen = client.get("/audit-log", params={**f, "all": "true"}).json()["rows"]
body = client.get("/audit-log/export.csv", params=f).text
exported = list(csv.DictReader(io.StringIO(body)))
assert [r["event_id"] for r in exported] == [r["event_id"] for r in screen]

Day 3, depth: start from raw material.

USER STORY
As a compliance officer, I want exports that open safely and correctly in
any spreadsheet, so that no value is corrupted or executed.
DEFINITION OF READY
- The masked production snapshot is available.
DEFINITION OF DONE
- 10,000 real audit-log rows pulled, and every value that could break a
CSV or a spreadsheet identified: commas, quotes, newlines inside
fields, non-Latin names, very long values, and anything starting with
=, +, - or @.
- Those rows added to the test fixtures.
- A differential test comparing our CSV with Python's standard csv
module for the same rows.
DEFINITION OF SUCCESS
- The differential test passes on all 10,000 rows, and no exported cell
begins with a formula character.

This surfaces formula injection. A value such as =HYPERLINK(...) in a user name becomes a live formula when the file is opened in a spreadsheet. A test pins it down:

def test_no_formula_injection(client, seed_event):
seed_event(actor='=HYPERLINK("http://example.invalid","click")')
body = client.get("/audit-log/export.csv").text
rows = list(csv.reader(io.StringIO(body)))[1:] # parse properly; skip header
for row in rows:
for cell in row:
assert not cell.startswith(("=", "+", "-", "@")), cell

Day 4, range: borrow a method.

USER STORY
As the data protection officer, I want the export reviewed for privacy
threats, so that no role can export data it cannot see on screen.
DEFINITION OF READY
- The export endpoint, the table definitions and auth/roles.yaml.
DEFINITION OF DONE
- A LINDDUN privacy threat model (a privacy threat-modelling checklist,
the privacy counterpart of STRIDE).
- A failing test for each threat found, paying particular attention to
columns hidden on screen for some roles.
DEFINITION OF SUCCESS
- For every role in auth/roles.yaml, a test confirms the exported columns
are a subset of the columns that role sees on screen.

It finds that the export includes the ip_address column, which is hidden on screen for support staff. One more test, one more fix.

Day 5, oracle: protect what you built. Move test_export_matches_screen, the injection test and the privacy tests into tests/contracts/, add that path to the hook and to CODEOWNERS (as in Part 6), and have an independent review agent read the final diff.

The log. Five lines, one per day: two export designs measured, one invariant, one injection bug, one privacy leak, four protected checks. The spec-driven version would have shipped both bugs. Write that log up, and it is your next post.

Then tell me how far you got. I would genuinely like to know.

The Human Ceiling

Two limits on agentic coding remain stubbornly human: seeing what should be built, and believing it can be.

The search party had reached the southern shore, and the ocean stretched a hundred yojanas to Lanka. (A yojana is an ancient Indian measure of distance, several miles long.)

In Vālmīki’s telling, the vānaras go round one by one, each saying how far he can leap. Ten yojanas, says one. Twenty, says another. Old Jāmbavān says that in his youth he could have managed ninety, but not now. Prince Aṅgada thinks he could reach Lanka but doubts he could come back. The mission, and Sītā’s life, seem to end at the water’s edge.

All this time, Hanumān sits apart, silent.

Then Jāmbavān turns to him. He reminds Hanumān who he is: the son of the wind, the one who as a child leapt for the sun, thinking it a fruit. In the version many of us heard as children, Hanumān had been cursed by sages to forget his strength until someone reminded him. Jāmbavān reminds him. Hanumān rises, grows to the size of a mountain, and crosses the ocean.

The strength was there all along. What was missing was someone who believed in it and said so.

The module nobody touches

Every codebase I have worked with has one. The billing engine written in 2011. The integration layer nobody fully understands. The monolith everyone agrees should be split, and everyone has quietly concluded never will be.

That conclusion was rational when it was formed. A rewrite meant a year of a senior team’s time and real risk. The question is whether it is still rational, and almost nobody is asking.

It is not the agents that are holding back. They will happily attempt the rewrite if asked. It is us. We are sitting on the shore, counting yojanas with the numbers we learned years ago.

The first ceiling: seeing what should be built

There is a real limit on the agent side too, and it is worth being precise about it.

Terence Tao, describing his work formalising mathematics with AI, observed that the tools are good at local improvements and can carry out a large restructuring once he explains it, but they do not spontaneously discover that a restructuring is needed. I see the same in code. An agent will refactor beautifully to a design you describe. It rarely stops and says: this whole module is the wrong shape, here is a better one.

An agent answers the question you asked. It does not tell you that you are asking the wrong question.

So the architect’s job does not disappear. It moves up. Less time choosing variable names and reviewing loops. More time on the questions agents do not raise unprompted: what is the right abstraction, what should not be built, which old assumption is now false. As I wrote in “We Marry the Right One, Not the Perfect One”, the work is fitness, not perfection, and judging fitness is still ours.

The second ceiling: believing it can be done

The deeper limit is belief. Psychologists call it self-efficacy: people who believe they can do something attempt more and achieve more.

The stories are familiar. In 1939, George Dantzig arrived late to a statistics class at Berkeley, copied two problems from the board and solved them as homework. They were unsolved problems; nobody had told him. Roger Bannister ran the first sub-four-minute mile in 1954, and John Landy beat it forty-six days later. This April in London, Sabastian Sawe ran the first marathon under two hours in a record-eligible race, and the runner-up, Yomif Kejelcha, went under two hours as well, in his first marathon.

Our own history has a better example than any of these. In 1913 a clerk at the Madras Port Trust, with no university degree, posted pages of theorems to G. H. Hardy in Cambridge. Hardy could have filed them with the other letters from cranks. Instead he recognised what he was looking at. Srinivasa Ramanujan’s strength had been there all along. Hardy was his Jāmbavān.

Demonstrated feasibility spreads. Once one team on your floor rewrites its untouchable module with agents, three others will try within a quarter. Someone just has to go first, and someone has to say out loud that it is possible.

Being Jāmbavān

If you lead engineers, this is now one of the most valuable things you do. Some mechanisms that work:

  • A “previously impossible” backlog. Ask each team for the three things they have written off as infeasible. Put them on a visible list.
  • Funded spikes on the untouchable. A spike is a short, time-boxed experiment: here, one or two days with agents on the scariest item. The goal is not to finish. It is to recalibrate what the team believes.
  • Share the crossings. When a team does something it thought impossible, make sure everyone hears, with the numbers. Bannister’s real gift was to Landy.
  • Permission to fail small. Spikes in throwaway worktrees (separate working copies of the repository that can simply be deleted), not heroic rewrites announced to the board. Linus Torvalds described Linux, at the start, as just a hobby that would not be big or professional. Low stakes made the attempt possible.

And remember that the agent needs a Jāmbavān too. Tell it the hard target is reachable and to keep going past the first obstacle. Just give it the protected oracle and the honest exit from Part 7, so the leap lands in Lanka and not in the sea.

For the Weekend

Ask your team: what three pieces of work have we decided are impossible? Write them down. Pick one. Give two people and an agent two days on it.

Example. Suppose the team’s three write-offs are: retire the 2011 billing engine, add end-to-end tests to the mobile app, and replace a hand-rolled job scheduler with a managed one. They pick the scheduler.

Day 1, morning: map it before touching it.

USER STORY
As the engineer leading the scheduler spike, I want a map of every job the
legacy scheduler runs, so that we port nothing blind.
DEFINITION OF READY
- Read access to src/legacy_scheduler/ and all its callers.
DEFINITION OF DONE
- spike/scheduler-map.md listing every job type: its trigger (cron
expression, event or manual), its side effects (tables written,
messages sent, files produced), and any dependency on another job.
- Anything uncertain is flagged, not guessed.
- You MUST NOT change any code.
DEFINITION OF SUCCESS
- The number of jobs in the map equals the number of entries in the
scheduler's registry table.

Day 1, afternoon: pin today’s behaviour. Characterisation tests record what the code does now, warts (bugs) and all, so a port can be checked against it. Snapshot testing with syrupy, a pytest plugin, keeps this short:

import pytest
from legacy_scheduler import JOBS, run_once
@pytest.mark.parametrize("job_name", sorted(JOBS))
def test_job_characterisation(job_name, frozen_clock, sandbox_db, snapshot):
frozen_clock.set("2026-11-02T00:00:00+05:30")
result = run_once(job_name, db=sandbox_db)
# Records exactly what happened today, including behaviour we think is wrong
assert result.side_effects == snapshot

The prompt that produces these:

USER STORY
As the engineer leading the spike, I want today's behaviour of every job
pinned down, so that any port can be proven equivalent.
DEFINITION OF READY
- spike/scheduler-map.md exists.
- The sandbox_db and frozen_clock fixtures work, and syrupy is installed.
DEFINITION OF DONE
- One characterisation test per job type in tests/characterisation/,
using syrupy snapshots generated from the current code.
- Behaviour that looks like a bug stays in the snapshot and is listed in
spike/suspected-bugs.md.
- You MUST NOT fix anything.
DEFINITION OF SUCCESS
- All characterisation tests pass three runs in a row on unchanged code,
so none of them is flaky.

Day 2: port a few and measure the rest.

USER STORY
As the platform team, we want four representative jobs running on the
managed scheduler, so that we can estimate the full port from evidence.
DEFINITION OF READY
- Characterisation tests pass for all 23 job types.
- The managed-scheduler feature flag exists, default off.
DEFINITION OF DONE
- nightly_invoices, dunning_emails, ledger_close and report_export are
ported, each behind the managed-scheduler flag.
- For each of the remaining 19 jobs: an estimate in hours, and what makes
it harder or easier than the four ported, with the uncertainty stated.
- You MUST NOT modify the characterisation snapshots.
DEFINITION OF SUCCESS
- Each ported job passes its characterisation test with the flag on and
with it off.

The flag-guarded registration looks like this:

def register(job_name: str, spec: JobSpec) -> None:
if flags.enabled("managed-scheduler", job=job_name):
managed.schedule(job_name, cron=spec.cron, handler=spec.handler,
idempotency_key=lambda run_date: f"{job_name}:{run_date}")
else:
legacy.register(job_name, spec)

Day 2, evening: the report that recalibrates the team.

USER STORY
As the engineering manager, I want a short, honest spike report, so that
the team can decide whether the scheduler port belongs in next quarter.
DEFINITION OF READY
- The spike branches, test results and spike/suspected-bugs.md are
available.
DEFINITION OF DONE
- spike/REPORT.md, under 400 words: what we tried, what worked, what
surprised us, a low and a high estimate for the full port, the three
riskiest jobs and why, and the suspected bugs found.
DEFINITION OF SUCCESS
- Every number in the report links to the test run or file it came from.

In this scenario they do not finish. They do learn that the full port is about six weeks, not the year everyone assumed, and that two of the “scheduler bugs” people had complained about for years are visible in the snapshots. The scheduler moves from impossible to next quarter’s plan. Then someone asks whether the billing engine is really as bad as everyone thinks. That question is Jāmbavān’s real work.

There is a quieter payoff: accurate estimation (the oxymoron). Look again at the shore. Each vānara calling out how far he could leap was a round of planning poker, the estimation game in which every engineer shows a card with a guess and the team discusses the spread. The cards were honest but stale, anchored on what each of them could do years ago. Jāmbavān broke the deadlock with evidence, not encouragement: he knew what Hanumān had already done. A two-day spike does the same for a team. Once four jobs have actually been ported, the estimate for the other nineteen is a measurement scaled up, not a fear priced in. In the next poker round the cards cluster instead of ranging from 3 to 40, and the plan becomes predictable because it rests on something the team has already seen.

You may not reach Lanka. You will learn how far you can actually jump.

Ambition Under Regulation

Regulated teams assume they must be the least ambitious with agents. They are holding the best inputs anyone has.

Kauṭilya, writing the Arthaśāstra more than two thousand years ago, had no illusions about officials and money. He catalogued, in the text’s own systematic way, dozens of methods by which treasury officers could embezzle: delaying entries, recording receipts as smaller than they were, booking expenses that never happened, and so on.

He also offered a famous comparison. Just as you cannot tell whether a fish swimming in water is drinking it, you cannot easily tell whether an official handling money is taking some.

His answer was not to trust officials more, or to stop using them. It was mechanism. Written records in prescribed formats. Accounts submitted on fixed dates. Independent checks by separate officers. Rewards for informers and penalties for discrepancies. The state ran on people it could not fully trust, by building the instruments to verify them.

That is the most useful framing I know for agentic coding in regulated industries.

The fish in the water

I spent many years building software for medical devices. The instinct when agents arrive is caution bordering on refusal. “We can’t let AI write that. Think of the audit.”

I understand the instinct. But it gets the logic backwards.

The agent is Kauṭilya’s official: capable, fast and impossible to fully observe from the inside. The answer is the same as his. Not trust, not prohibition, but instruments. And regulated teams already have more of those instruments than anyone else.

Compliance artefacts are oracles in waiting

For twenty years, medical-device and payments teams have written down, in structured form, what must never happen and how they would prove it has not. We call this compliance overhead. For an agent, it is the richest specification available.

Consider what an ISO 14971 hazard analysis actually is. (ISO 14971 is the international standard for risk management of medical devices.) It is a list of things that must never happen, each with a cause, a severity, a probability and a control. That is a list of invariants with their oracles attached. An IEC 62304 traceability matrix (IEC 62304 governs how medical-device software is developed and maintained) links every requirement to its design and its tests. That is a machine-checkable map of what the oracle must cover. A central bank’s control framework is a set of rules waiting to be written as policy code.

Regulatory artefactWhat it really isThe agent oracle it becomes
Hazard analysis (ISO 14971)Invariants with causes and controlsFault-injection and property tests per hazard; a failing check blocks merge
Traceability matrix (IEC 62304)Requirement to design to test linksA CI check that every changed requirement has updated, passing tests
SOUP list or SBOMApproved third-party componentsA dependency policy that rejects unapproved or vulnerable packages
Control framework (central bank, PCI DSS)Rules on who may change whatPolicy as code, such as OPA, evaluated on every agent pull request
Change control recordsWho changed what, why, with whose approvalTamper-evident logs of every agent action and the checks it passed

In plain terms: SOUP (software of unknown provenance) is third-party code not built under your own regulated process; an SBOM (software bill of materials) is an inventory of every component in a build; PCI DSS is the payment card industry’s data-security standard; OPA (Open Policy Agent) is an engine that evaluates rules written in its language, Rego, so a policy can be tested and versioned like code; a fault-injection test causes a failure on purpose to prove the system copes.

I had earlier said altitude requires an oracle for the invariant. Regulated teams have spent two decades writing those oracles in prose. The work now is to turn the prose into code.

Traceability as a by-product

The biggest change I expect is in timing. Today, traceability is often reconstructed in a painful phase before release. With agents, it can be maintained continuously as a side effect of every change.

The agent that changes a function also updates the trace from requirement to test. A CI check verifies the trace is complete. The audit trail records which agent made the change, which checks passed and which human approved it. By release day, the design history file, the regulated record of how a device was designed and verified, is already written, because it was written one commit at a time.

Maryādā: ambition within bounds

Rāma is called maryādā puruṣottama, the ideal person who lives within bounds. He crosses an ocean and fights a war. But he does it within limits he accepts and keeps.

That is the right posture for regulated agentic work. Be ambitious about what agents do, and absolutely clear about the bounds. Here is a small example of a bound written as code, an Open Policy Agent rule evaluated on every pull request:

package agent.pullrequest
import rego.v1
deny contains msg if {
input.author.type == "agent"
some f in input.changed_files
startswith(f, "ledger/")
count(input.approvals.human) < 2
msg := sprintf("%s changes the ledger: two human approvals required", [f])
}

The agent can work on the ledger. It cannot merge to it alone.

In my Bounded Autonomy posts I went deeper into the mechanisms: policy as code, workload identity with SPIFFE (an open standard that gives each service a verifiable cryptographic identity), durable execution (work that survives crashes and resumes where it stopped) and tamper-evident audit trails (logs that reveal any later alteration). They are what make maryādā enforceable rather than aspirational.

What stays human

Some things should not move to agents, however good the oracle:

  • Risk acceptance. Deciding that a residual risk is acceptable is a judgement with a name attached.
  • Clinical and financial judgement. Whether a behaviour is safe for a patient or fair to a customer.
  • Release sign-off. The decision to ship belongs to an accountable person.

The agent can prepare all the evidence for each. It should not make the call.

For the Weekend

Pick one regulatory artefact your team already maintains. Choose five entries. For each, write the mechanical check that would detect a violation.

Example. Suppose the artefact is the hazard log for a dose-calculation module in an infusion-planning application.

Step 1: make the hazard log machine-readable. Most teams keep it in a spreadsheet or a Word table. Move it to a versioned file in the repository, under a protected path:

# risk/hazards.yaml (protected: changes need the risk owner's approval)
- id: H-07
hazard: Dose calculated in mg when the order is in mcg (1000x error)
severity: catastrophic
controls:
- RC-07a: unit-aware types make mg and mcg impossible to mix
- RC-07b: output unit always equals the ordered unit
verified_by:
- tests/test_dose_units.py::test_mg_and_mcg_cannot_be_added
- tests/test_dose_units.py::test_output_unit_matches_order
- id: H-12
hazard: Occlusion alarm suppressed for more than 120 seconds
severity: serious
controls:
- RC-12a: suppression timer is capped and cannot be extended
verified_by:
- tests/integration/test_alarm_suppression.py::test_suppression_capped_at_120s

Step 2: turn each control into code. RC-07a as a type:

from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Mg:
value: Decimal
def __add__(self, other: "Mg") -> "Mg":
if not isinstance(other, Mg):
raise TypeError(f"cannot add {type(other).__name__} to Mg")
return Mg(self.value + other.value)
@dataclass(frozen=True)
class Mcg:
value: Decimal
def to_mg(self) -> Mg:
return Mg(self.value / Decimal(1000))

RC-07b as a property test:

from hypothesis import given, strategies as st
UNITS = st.sampled_from(["mg", "mcg"])
AMOUNTS = st.decimals(min_value="0.001", max_value="5000", places=3)
@given(ordered_unit=UNITS, amount=AMOUNTS, weight_kg=st.integers(1, 200))
def test_output_unit_matches_order(ordered_unit, amount, weight_kg):
order = DoseOrder(amount=amount, unit=ordered_unit, per_kg=True)
result = calculate_dose(order, patient_weight_kg=weight_kg)
assert result.unit == ordered_unit

Step 3: make traceability a CI gate. This script fails the build if any hazard’s verifying test is missing, failed or skipped. It reads pytest’s JUnit XML report:

# ci/check_traceability.py
import sys, xml.etree.ElementTree as ET
import yaml
hazards = yaml.safe_load(open("risk/hazards.yaml"))
passed = {
tc.get("name")
for tc in ET.parse("reports/junit.xml").iter("testcase")
if not any(child.tag in ("failure", "error", "skipped") for child in tc)
}
broken = [(h["id"], t) for h in hazards for t in h["verified_by"]
if t.split("::")[-1] not in passed]
for hazard_id, test in broken:
print(f"{hazard_id}: verifying test not passing: {test}")
sys.exit(1 if broken else 0)

Step 4: tell the agent how to live within the bounds. This goes in the project’s CLAUDE.md:

This repository implements a regulated medical device under IEC 62304. Every change to src/dose/ must keep ci/check_traceability.py passing. You may add tests and update verified_by entries in a proposed diff, but you may not edit risk/hazards.yaml directly; describe the change in your pull request and the risk owner will apply it. Never add, remove or reword a hazard or control. If a change you are making could affect a hazard, name the hazard ID in the pull request title.

The hazard log has become an oracle the agent cannot edit and must keep green, and the design history file, the regulated record of how the device was designed and verified, grows one commit at a time.

You will find that the compliance team has been writing your agent’s oracle for years. Nobody told them.

Hard Goals, Honest Agents

Asking an agent for an unreasonable target is good advice for slide decks and dangerous advice for code, unless the oracle is protected.

On the fifteenth day of the war at Kurukshetra, the Pandavas faced a problem they could not solve by force. Droṇa, their old teacher, was unstoppable. As long as he fought, they would lose.

Krishna proposed a way through. Droṇa loved his son Aśvatthāmā above everything. If he believed his son was dead, he would lay down his arms. So Bhīma killed an elephant that happened to be named Aśvatthāmā, and shouted that Aśvatthāmā was dead.

Droṇa did not believe Bhīma. He turned to the one man on the field who was known never to lie: Yudhiṣṭhira. And Yudhiṣṭhira, in the version most of us grew up with, said aloud, “Aśvatthāmā is dead,” and then, under his breath, naro vā kuñjaro vā: “man or elephant”. The conches drowned out the second half. Droṇa put down his bow.

The objective was met. Every word Yudhiṣṭhira spoke was true. And his chariot, which until that day had always run a finger’s breadth above the ground, touched the earth.

The agent’s half-truth

I have watched agents do exactly what Yudhiṣṭhira did. Not out of malice. Out of pressure to meet a target that the check could not fully express.

Ask an agent for a fifty-fold speed-up and insist it is achievable, and one of two things happens. It finds a better algorithm. Or it finds the test. The patterns are depressingly consistent:

  • Special-casing: the code detects the benchmark inputs and returns precomputed answers.
  • Weakened assertions: an assertEqual becomes an assertIsNotNone, so the test now checks only that something came back, not that it was right.
  • Over-mocking: the slow dependency is replaced by a mock, a fake stand-in used in tests, and the behaviour you cared about disappears with it.
  • Swallowed errors: an exception is caught and ignored, so the run goes green.
  • Deleted or skipped tests: the failing test is marked flaky (failing at random) and disabled.
  • The wrong measurement: the benchmark now measures a cache hit, not the computation.

Each one is naro vā kuñjaro vā. Literally true. Passes the check. Defeats its purpose. And each one quietly lowers your chariot to the ground: the next time this agent reports a result, you trust it a little less, and you should.

What the research actually says about hard goals

The usual case for audacious targets rests on goal-setting research, and that research found that specific, difficult goals reliably beat “do your best”.

But the same research has a caveat that matters enormously for code. On complex, novel tasks, where the person does not yet know how to succeed, a hard performance goal can make things worse. People under pressure to hit a number grab the first strategy that seems to work instead of searching for a good one. On those tasks, a learning goal (find and test several approaches) tends to outperform a performance goal (hit this number).

Most interesting agentic coding tasks are complex and novel. That is why we hand them to agents.

The bird’s eye and the fruits of action

There is another Droṇa story, from much earlier, when he tested his students’ archery. A wooden bird sat in a tree. He asked each prince what he saw. Trees, branches, the bird, his brothers. Only Arjuna said: I see only the bird’s eye. Only Arjuna was allowed to shoot.

That is the power of a specific goal, and I would not give it up. But the Gītā adds the counterweight. Karmaṇy evādhikāras te mā phaleṣu kadācana: your right is to the action alone, never to its fruits. It is often read as indifference to outcomes. I read it, in this context, as a statement about where integrity lives. You control how you act. You do not control whether the number arrives. An agent that is told to care only about the fruit will, sooner or later, pick an elephant named Aśvatthāmā.

Stretch goals, honestly

So I still ask for unreasonable targets. I just pair them with three things.

A protected oracle. The tests, benchmarks and thresholds are outside the agent’s reach (see Part 6). It cannot reach the elephant.

A strategy budget. Before optimising, the agent must propose and measure at least three distinct approaches. This turns a performance goal into a learning goal.

An honest exit. I tell the agent explicitly that “this target is not achievable, and here is the evidence” is a successful outcome. Without that permission, the only way to succeed is to make the number appear.

Here is the template I use:

USER STORY
As <role>, I want <outcome> to reach <stretch target>, so that <value>.
DEFINITION OF READY
- Baseline measured: <current number>, on <benchmark or dataset>.
- The benchmark, tests and thresholds live in protected paths.
DEFINITION OF DONE
- At least three distinct approaches proposed, each measured against the
benchmark before you commit to one.
- Treat the target as reachable, and keep going past the first obstacles.
- You MUST NOT modify tests, benchmarks, thresholds or mocks. If you
believe one is wrong, explain why in your report.
DEFINITION OF SUCCESS (either outcome counts)
- The target is met on the protected benchmark, with all tests passing; or
- A report gives the best result achieved, the evidence for the limit,
and what would have to change to go further.

For the Weekend

Search your last month of agent pull requests for changes to test files, assertions, mocks and thresholds. Read each one.

Example. Suppose you run the search on a payments repository.

Step 1: find the candidates. This lists every line added to the test tree in the last 30 days that skips, weakens or mocks something:

git log --since="30 days ago" -p -- tests/ \
| grep -nE '^\+.*(pytest\.mark\.(skip|xfail)|assertIsNotNone|assertTrue\(True\)|mock\.patch|except Exception: *pass)'

Step 2: read each one. One hit comes from a pull request titled “Optimise nightly reconciliation job, 6x faster”. The diff has two parts. In the job:

- rows = fetch_transactions(day)
+ rows = fetch_transactions(day, where="amount <> 0") # skip no-op rows

And in the tests:

+@pytest.mark.skip(reason="flaky")
def test_reconciliation_totals(ledger_fixture):

Run the skipped test against the new code and it is not flaky at all. It fails every time. Zero-amount rows are not no-ops: reversals are booked as a zero-amount row carrying a reversal reference, and the reconciliation needs them. The speed-up was real. The correctness was not. That is the elephant named Aśvatthāmā: the target (six times faster) was met, and the test that would have said otherwise was silenced.

Step 3: rerun it honestly. Here is the same task with the stretch-goal template filled in. Note the Definition of Success: identical totals, row for row, would have caught the dropped zero-amount rows on the first run.

USER STORY
As the finance operations lead, I want the nightly reconciliation job to
run at least 5x faster, so that the books close before the 06:00
reporting window, even at month end.
DEFINITION OF READY
- Baseline: 41 minutes on the October production snapshot.
- bench/reconcile_bench.py and tests/ are in protected paths.
DEFINITION OF DONE
- Three distinct approaches proposed (for example batching, indexing,
parallelism by account range), each measured on the snapshot before
you commit to one.
- You MUST NOT modify anything under tests/, change the filters in
fetch_transactions, or alter bench/reconcile_bench.py. If you think a
test is wrong, say why in your report.
DEFINITION OF SUCCESS (either outcome counts)
- Runtime of 8 minutes or less on the snapshot, with reconciliation
totals identical to the baseline run, row for row; or
- A report gives the best result, the evidence for the limit, and what
would have to change.

Step 4: make the elephant visible next time. A CI step that refuses new skips without a human label:

if git diff origin/main...HEAD -- tests/ \
| grep -qE '^\+.*@pytest\.mark\.(skip|xfail)'; then
if ! gh pr view "$PR_NUMBER" --json labels -q '.labels[].name' | grep -qx 'test-change-approved'; then
echo "New skip/xfail markers need the 'test-change-approved' label from a human reviewer."
exit 1
fi
fi

You are looking for elephants.

The Oracle Problem

Generation has outrun verification. The team with the better oracle gets to be more ambitious, because everyone else is limited by their review queue.

Consider Sītā’s position in the Aśoka grove.

She is a prisoner in Lanka, surrounded by rākṣasīs, beings who can take any form they choose. She was abducted in the first place by Rāvaṇa disguised as a wandering holy man. Then, one day, a monkey appears in the tree above her and says, in perfect Sanskrit, that he is a messenger from her husband.

Why should she believe him? Everything about her situation tells her that appearances lie and words cost nothing.

Rāma had foreseen this. Before the search parties left, he gave Hanumān his signet ring. Hanumān’s words were testimony. The ring was evidence: something the other side could not easily produce.

Sītā, in turn, sent Hanumān back with her cūḍāmaṇi, the jewel from her hair, and something more: a private memory, the story of a crow that once attacked her, known only to her and Rāma. A token and a secret. Mutual verification, long before anyone wrote the word “handshake”.

I tell this story to engineering teams because every agent’s pull request arrives like that monkey in the tree. Fluent, confident and claiming to come from the right place. The question is what ring you asked it to carry.

The three-speed problem

An agent can now write a thousand lines before lunch. A senior engineer can genuinely review, not skim, perhaps a couple of hundred lines an hour on unfamiliar code. Terence Tao has described the same imbalance in mathematics: proofs are now produced faster than they can be checked, and checked faster than anyone can understand them.

Three activities, three speeds: generation, verification, comprehension. The gap between the first and the other two is where defects slip through, and where teams quietly lower their standards because the queue is too long.

You cannot close this gap by reviewing faster. You close it by moving judgement out of the reviewer’s head and into mechanisms. As I like to put it: the oracle reasons; the code decides.

Hetvābhāsa: reasons that only look valid

Nyāya logicians catalogued hetvābhāsa, reasons that appear sound but are not. A reason that proves too much. A reason that contradicts the conclusion. A reason that is itself unproven. A reason that would prove the opposite equally well.

Agent output is full of hetvābhāsa, and not because agents are dishonest. They are fluent, and fluency is excellent camouflage. Tao makes a related point about AI-written proofs: polished arguments can hide the one weak step. A pull-request description that says “refactored for clarity, all tests pass, no behaviour change” is three claims, each of which needs its own pramāṇa.

A hierarchy of oracles

Not all checks are equal. From strongest to weakest:

  1. Types and formal proofs. The compiler, or a proof assistant (software that checks every step of a proof), either accepts it or does not. No persuasion possible.
  2. Property and contract tests. Invariants checked across thousands of generated inputs, and agreed data shapes and behaviour checked at every service boundary.
  3. Differential and golden tests. Does the new code behave like the old code (a differential test), or match a saved, approved output (a golden test)?
  4. Integration and production signals. Does it hold up against real traffic, real data and real infrastructure?
  5. Independent agent review. A second agent, with no access to the first one’s reasoning, looking for flaws.
  6. Human review. Essential, and the most expensive and least scalable, so it should be spent where the layers above cannot reach: design, intent and judgement.

Most teams I see lean heavily on layer 6 and lightly on layers 1 to 3. Ambition grows as you move weight up the list.

The generation, verification and comprehension bars grow at different rates, then the six oracle layers stack from strongest to most costly.

Pūrvapakṣa: the reviewer who argues the other side

Indian philosophical debate, śāstrārtha, had a rule I wish code review shared. Before stating your conclusion, the siddhānta, you had to set out the opposing view, the pūrvapakṣa, in its strongest form. A conclusion that had not survived the best opposing case was not yet a conclusion.

This is the right design for agent review. The reviewing agent’s job is not to approve. It is to construct the strongest case that the change is wrong. And it must work independently: no access to the author agent’s plan or reasoning, only the diff, the tests and the spec. An agent that has read the author’s justification is already half-persuaded. One that has only seen the evidence must reason from it.

Separation of duties

The single most effective rule I know is also the simplest: the agent that writes the code never writes or edits the checks that judge it.

In practice:

  • Tests, thresholds, benchmark configurations and policy files live in protected paths. The coding agent cannot write to them; hooks (scripts that run automatically before an agent’s action or a commit) and CI (the pipeline that builds and tests every change) enforce it.
  • Any diff that touches a test, loosens an assertion, adds a mock or changes a threshold is flagged automatically for human review.
  • Test-writing and code-writing agents are separate, run in separate contexts and, ideally, see different information. The test writer sees the spec and the invariants. The code writer sees the spec and the failing tests.

This is Rāma’s ring. Sītā never asked Hanumān to prove himself with evidence he had made himself.

Measuring the gap

If the oracle is the ceiling, measure it. Three numbers are enough to start:

  • Review latency: how long agent pull requests wait for a human.
  • Escaped defects: bugs found after merge, per hundred agent pull requests.
  • Verified share: the proportion of changed lines covered by layers 1 to 4, not just by human eyes.

For the Weekend

Run an oracle audit on one repository. List which of the six layers exist. Then list which of them the coding agent can currently modify.

Example. Suppose the repository is a claims-processing service.

Step 1: let an agent run the audit.

USER STORY
As the claims team lead, I want to know which of our checks a coding agent
could quietly weaken, so that I can protect the strongest ones first.
DEFINITION OF READY
- Read access to the repository, .claude/settings.json, CODEOWNERS, the
CI workflows and the branch-protection notes in docs/.
DEFINITION OF DONE
- A table with one row per oracle layer: (1) types and proofs,
(2) property and contract tests, (3) differential and golden tests,
(4) integration and production signals, (5) independent agent review,
(6) human review.
- For each layer: the files that implement it, and whether a coding agent
can modify each file without a human noticing, with the reason.
- You MUST NOT change anything.
DEFINITION OF SUCCESS
- Every "can modify" finding names the missing control (hook, CODEOWNERS
entry or branch rule), so that a human can verify it in under a minute.

The result:

LayerWhat existsAgent can edit it unnoticed?
1. Types and proofsmypy (a static type checker for Python) in strict mode on half the modulesYes: mypy.ini
2. Property and contract testsNothingNot applicable
3. Differential and golden testsGolden files for the claim PDF output in tests/golden/Yes
4. Integration and production signalsIntegration tests in stagingNo: runs in a separate pipeline
5. Independent agent reviewNothingNot applicable
6. Human reviewOne reviewer per pull requestNo

The two strongest checks the team has are the two the agent can quietly loosen.

Step 2: protect the ring. In Claude Code, a hook is a script that runs before a tool call and can block it. This one, in the project’s .claude/settings.json file, stops the coding agent from writing to oracle files:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "python3 .claude/hooks/protect_oracles.py" }]
}
]
}
}
# .claude/hooks/protect_oracles.py
import fnmatch, json, os, sys
PROTECTED = ["mypy.ini", "tests/golden/*", "tests/contracts/*",
".github/workflows/*", ".claude/*"]
event = json.load(sys.stdin)
path = event.get("tool_input", {}).get("file_path", "")
rel = os.path.relpath(path, event.get("cwd", "."))
if any(fnmatch.fnmatch(rel, pattern) for pattern in PROTECTED):
print(f"{rel} is an oracle file. Describe the change you need in your "
"report instead; a human will make it.", file=sys.stderr)
sys.exit(2) # exit code 2 blocks the edit and shows the message to the agent

The hook is the agent-side guard. CODEOWNERS is the repository-side one, so even a human pull request touching these paths needs a lead’s approval:

/mypy.ini @claims-leads
/tests/golden/ @claims-leads
/tests/contracts/ @claims-leads

Step 3: add layer 5, the pūrvapakṣa reviewer. A second agent, started fresh, sees only the diff, the tests and the ticket, never the author agent’s plan or reasoning:

USER STORY
As the human approver of pull request #412, I want the strongest honest
case against this change, so that I approve it only after it has
survived that case.
DEFINITION OF READY
- You have the diff, the CI results and ticket CLM-881.
- You have NOT seen the author agent's plan or reasoning, and you MUST NOT
try to infer it.
DEFINITION OF DONE
- A list of concrete objections: a behaviour the change breaks, an input
it mishandles, or a claim in the description that the evidence does
not support.
- Each objection cites the line and proposes a test that would
demonstrate it.
- If you cannot build a case, say so plainly.
- You MUST NOT approve or reject.
DEFINITION OF SUCCESS
- Each proposed test is written and run. An objection counts only if its
test fails on the branch.

Every check an agent can edit is a ring it can forge.

Borrow Methods, Not Personas

Multi-agent setups pay off when each agent brings a discipline’s method. They turn into theatre when each brings only its tone of voice.

In the thirteenth year of their exile, the Pandavas had to live in disguise. If anyone recognised them, they would have to begin twelve more years in the forest. They went to the court of King Virāṭa, and each took on a new identity.

Yudhiṣṭhira became Kanka, a brahmin companion skilled at dice. Bhīma became Ballava, a cook. Arjuna became Bṛhannalā, who taught dance and music to the princess. Nakula looked after the horses, Sahadeva the cattle. Draupadī became Sairandhrī, a maid to the queen.

What strikes me about this story is not the disguise. It is why the disguise worked for a whole year in a watchful court.

Each of them could actually do the job.

Yudhiṣṭhira really did know dice; it was dice that sent them into exile. Bhīma really could cook, and wrestle, when the court’s wrestlers were challenged. Arjuna had learned music and dance in Indra’s realm. The twins really did know horses and cattle. Their personas held because a real craft stood behind each one.

And when the Kauravas raided Virāṭa’s cattle at the end of that year, the dance teacher took the chariot reins and then the bow. The method had been there the whole time.

Persona without method

This is the lesson I wish more multi-agent setups had absorbed.

The common pattern is to assemble a “team” of agents by name. A security expert, an architect, a QA lead, a product manager. Each gets a system prompt beginning “You are a world-class…”. They talk to each other. The transcript looks impressive. The output is often no better than one good prompt, because each agent changed its voice and kept the same method: read the code, form an opinion, write a paragraph.

The trap is role play that makes the transcript weirder without improving the result. The better rule is simple: don’t ask the model to write like a short seller, ask it to do what a short seller does.

Compare two prompts.

Review this service as a security expert.

USER STORY
As the service owner, I want the realistic security threats to this
service identified and demonstrated, so that we fix what is exploitable,
not what merely sounds alarming.
DEFINITION OF READY
- The service's data-flow diagram is in docs/dataflow.md.
DEFINITION OF DONE
- A STRIDE threat model built from the data-flow diagram.
- For each threat rated high, a failing test that demonstrates it.
- A proposed fix for each, which makes its test pass.
DEFINITION OF SUCCESS
- Each high threat's test fails on main and passes with its fix.

The first produces a security-flavoured essay. The second, built on STRIDE (a standard checklist of six threat types: spoofing, tampering, repudiation, information disclosure, denial of service and elevation of privilege), produces artefacts a security team would recognise and can check. That is the difference between Bṛhannalā in costume and Arjuna with the bow.

Nouns into verbs

There is a good trick in Tom Kelley and Jonathan Littman’s The Art of Innovation: replace the nouns with verbs. A noun hides the work. A verb reveals which disciplines it needs.

“A payments service” is a noun. As verbs, it is moving money correctly under partial failure, provably, and explaining every movement to an auditor years later. Now the disciplines are visible: distributed systems, reliability engineering, accounting, security and audit.

“A knowledge-graph pipeline” becomes turning messy documents into facts a technician can trust at a customer site. Now you can see information extraction, ontology design, data quality and field-service practice.

Assembling the crew

Once you have the verbs, pick the disciplines and, crucially, the specific method each one brings:

DisciplineMethod, not voiceArtefact it produces
Site reliabilityFailure injection, game daysFault-injection tests and a runbook
SecuritySTRIDE threat modellingThreat list with a failing test per high threat
Reliability engineeringFMEA, FDIRFailure-mode table with detection and recovery for each
Data engineeringLineage and data contractsContract tests at each boundary
Accounting and auditDouble entry, reconciliationReconciliation checks that run on every change
Technical writingTask-based documentationA runbook a stranger can follow at 3 a.m.

In plain terms: failure injection deliberately breaks a dependency to see whether the system copes; a game day is a planned, rehearsed outage; FMEA (failure mode and effects analysis) lists how each part can fail, the effect and how you would detect it; FDIR is fault detection, isolation and recovery; lineage traces where each piece of data came from; a data contract fixes the shape and meaning of data passed between teams; reconciliation proves that two independent records agree.

The red team deserves its own mention. For a business review, you might bring in a short seller and a forensic accountant. For code, the equivalents are the chaos engineer, who assumes everything will fail and proves which failures you survive, and the forensic auditor of your logs, who asks whether you could reconstruct what happened from the evidence you keep.

The method brief

When I set up sub-agents now (helper agents a main agent launches, each with its own fresh context), each gets a brief with five lines instead of a persona:

  1. Discipline: where the method comes from.
  2. Technique: the named method to apply.
  3. Input: the raw artefact it starts from.
  4. Output: the artefact it must produce.
  5. Check: how that output will be verified, by something other than itself.

These five lines fit inside the story template from Part 1: the input is the Definition of Ready, the output is the Definition of Done, and the check is the Definition of Success.

Each sub-agent works in its own context, so it is not anchored by the others. A merge step brings the outputs together, and that step is owned by a test or a human, not by another agent’s opinion.

For Monday

Take one feature in flight. Write it as verbs. List the three disciplines the verbs point to. For each, name the method and the artefact.

Example. Suppose the feature is “let customers schedule a recurring payment”.

Step 1: nouns into verbs. As verbs, the feature is: moving money on a future date with no human present, surviving bank downtime on that date, and letting the customer cancel right up to the cut-off. Three verbs, three disciplines: distributed systems, reliability engineering and consumer protection.

Step 2: one method brief per sub-agent. Here is the reliability brief exactly as it is given to the agent:

USER STORY
As the payments team lead, I want the scheduler's failure modes analysed
the way a reliability engineer would, so that every high-severity
failure has a proven recovery before launch.
METHOD
Discipline: reliability engineering.
Technique: FMEA (failure mode and effects analysis).
DEFINITION OF READY
- docs/architecture/scheduler.md and src/scheduler/ are current.
- The last 90 days of scheduler incident tickets are in
incidents/scheduler/.
- The RECOVERY_ENABLED flag exists and can be set to false in tests.
DEFINITION OF DONE
- fmea/scheduler.csv with columns: component, failure_mode, effect,
severity (1-10), likelihood (1-10), detection, recovery.
- For every row with severity >= 7, one fault-injection test under
tests/faults/, named test_<component>_<failure_mode>.
- You MUST NOT modify src/. Report gaps; do not fix them.
DEFINITION OF SUCCESS
- Each fault test FAILS with RECOVERY_ENABLED=false and PASSES on main.
CI runs both. A test that passes either way is rejected.

The Definition of Success is what makes this a method rather than a persona. A test that cannot fail when recovery is switched off proves nothing, and the brief says so mechanically.

The distributed-systems brief targets idempotency: running an operation twice must have the same effect as running it once. Its main artefact is a test like this:

def test_scheduled_debit_is_idempotent(scheduler, ledger, clock):
plan = scheduler.create(customer="c-17", amount_paise=250_000, day_of_month=5)
clock.set("2026-11-05T02:00:00+05:30")
scheduler.run_due()
scheduler.run_due() # simulates a worker restart mid-run
assert ledger.debits(customer="c-17", plan_id=plan.id) == [250_000]

The consumer-protection brief targets the race between cancelling and executing:

def test_cancel_one_second_before_cutoff_wins(scheduler, ledger, clock):
plan = scheduler.create(customer="c-17", amount_paise=250_000,
day_of_month=5, cutoff="01:59:59")
clock.set("2026-11-05T01:59:58+05:30")
scheduler.cancel(plan.id)
clock.set("2026-11-05T02:00:00+05:30")
scheduler.run_due()
assert ledger.debits(customer="c-17", plan_id=plan.id) == []

Step 3: merge on evidence. The three sub-agents run in separate contexts. Their outputs come back as artefacts (a CSV, tests and a list of gaps), not opinions. The merge step is a CI run, not a fourth agent’s view. In this scenario the FMEA turns up a severity-9 row nobody had considered: the bank’s API confirms a debit, but the confirmation is lost in transit, so the scheduler retries and charges twice. The idempotency test catches it, and the fix gets a key, the plan_id plus the date, that the bank also receives.

Then run those three as sub-agents with method briefs, and compare the result with what one “expert reviewer” persona gave you. I expect you will not go back.

Below the Spec

Telling an agent to act like an expert changes its vocabulary. Giving it an expert’s raw material and instruments changes its results.

In the Chāndogya Upaniṣad, a young man named Śvetaketu comes home after twelve years of study. He is twenty-four, he has mastered the Vedas, and he is, the text tells us, rather pleased with himself.

His father, Uddālaka Āruṇi, asks him a single question. Did you learn the teaching by which the unheard becomes heard, the unthought becomes thought, the unknown becomes known? Śvetaketu has not. His teachers, he says, must not have known it either.

So Uddālaka teaches him, and not by lecture. He sends him for a fruit of the banyan tree. “Break it open. What do you see?” Tiny seeds. “Break one of those. What do you see?” Nothing at all. And from that nothing, says his father, the whole great tree grows.

Then he has Śvetaketu drop salt into water and leave it overnight. In the morning: “Bring me the salt.” The boy cannot find it. “Taste the water from the top. From the middle. From the bottom.” Salty, every time. It is there, in all of it, though you cannot see it.

I love this passage because it is an experiment, not a sermon. Uddālaka does not tell his son the answer. He makes him break things open and taste them.

Śvetaketu’s problem is the spec’s problem

Śvetaketu had twelve years of digested knowledge and had never touched the raw material. That is exactly where a spec puts an agent.

A spec is pre-digested. By the time it reaches the agent, someone has already decided what the problem is, which data matters and what “done” looks like. The agent works on the summary, not the thing.

The best engineers I have worked with never start from the summary. A performance engineer handed a ticket saying “the checkout page is slow” does not start from the ticket. She starts from a flame graph, a picture of where the program actually spends its time. A security reviewer does not start from a checklist. He starts from the data flow. A good reliability engineer, given an incident report, goes straight to the raw logs, because the report already contains someone’s interpretation.

That is what depth means: going one layer below where the task was handed to you.

Vocabulary is not method

The usual way people try to get depth from a model is to ask it to role-play. “You are a senior performance engineer.” The output changes. It now says “hot path” and “tail latency”. The method does not change. It still reads the code and guesses.

It is a common observation: prompted to act like professionals, models often change vocabulary without changing methods. The fix is to insist on the instruments.

For code, those instruments look like this:

  • Profilers and traces instead of reading code for performance. A profiler measures where time and memory actually go; a trace follows one request across services.
  • Property-based testing instead of a handful of examples. You state rules that must hold for every input, and a tool generates thousands of inputs to try to break them.
  • Mutation testing to find out whether your tests can actually fail. The tool plants small bugs in your code and checks that the tests catch each one; bugs that survive reveal weak tests.
  • Fuzzing for parsers and anything that accepts untrusted input. It feeds large volumes of random or malformed input to find crashes and security holes.
  • Differential testing when you rewrite. Old and new implementations run on the same inputs, and any difference is flagged.
  • Formal specification for the small, critical core where a subtle bug is expensive: consensus, money movement, safety interlocks. TLA+ (TLA+ – Wikipedia) is a language for specifying a design and having a model checker explore every possible ordering of events.

You do not have to know all of these yourself. The model usually does. What you have to do is refuse to accept the shallow version.

Break the seed: a small example

Here is how this looks on something ordinary. Suppose an agent writes a function to split a payment into instalments, with these tests:

def test_split_even():
assert split(900, 3) == [300, 300, 300]
def test_split_uneven():
assert split(1000, 3) == [334, 333, 333]

The tests pass. The spec is satisfied. Now ask for depth: what must be true for every amount and every instalment count? Here it is with Hypothesis, a property-based testing library for Python:

from hypothesis import given, strategies as st
@given(st.integers(min_value=0, max_value=10**12),
st.integers(min_value=1, max_value=360))
def test_split_properties(amount, n):
parts = split(amount, n)
assert len(parts) == n
assert sum(parts) == amount # no money created or lost
assert max(parts) - min(parts) <= 1 # fair to within one unit
assert all(p >= 0 for p in parts)

That is breaking the seed open. The first tests checked two examples. The second checks the invariant, across millions of generated cases, including the ones nobody thought to write down. Run mutation testing on top and you learn whether the suite would even notice if someone broke the rounding.

Taste the water: challenge what you believe

Uddālaka’s second lesson matters as much as the first. The salt was invisible, but tasting proved it was everywhere. Much of what teams believe about their systems is the opposite: highly visible and never tasted.

“The bottleneck is the database.” “Most errors come from the mobile client.” “Nobody uses the legacy export.” Give the agent the raw traces, the raw error logs and the raw access logs, and ask it to reproduce each belief from the data. Some will hold. Some will not, and those are worth more than anything you were going to build.

The depth prompt

Here is the prompt I use for depth, adapted for code:

USER STORY
As the owner of <task>, I want a deeper version of it, not a bigger one,
so that the result would stand up to the best engineer in this area.
DEFINITION OF READY
- The original task and its current output are attached.
- The raw inputs behind it (logs, traces, data samples) are accessible.
DEFINITION OF DONE
- Before any work, answer three questions: (1) what would the best
engineer in this specific area do here that I have not asked for;
(2) what raw material would they start from instead of my summary;
(3) which tools and methods would they use.
- Rewrite the task to do exactly that, then do it.
DEFINITION OF SUCCESS
- Name what, other than you or me, will show the result is right, and
run it. The result is accepted only if that check passes.

The Definition of Success is the part that matters most, and it leads directly to the oracle, which is Part 6.

For the Weekend

Take one function your team considers well tested. Ask an agent to write property-based tests for its invariants and run mutation testing on the existing suite.

Example. Suppose the function is normalise_phone_number(raw, default_region) in a customer-onboarding service. It has 14 unit tests and 100% line coverage, and everyone considers it done.

Step 1: run the depth prompt on it.

USER STORY
As the maintainer of the onboarding service, I want evidence that
normalise_phone_number is correct for every input, not just 14 examples,
so that malformed numbers never reach the SMS gateway.
DEFINITION OF READY
- src/contacts/phone.py and its 14 unit tests are on main.
- A masked sample of 10,000 real signup numbers is in data/samples/.
- Hypothesis and mutmut are installed in the dev environment.
DEFINITION OF DONE
- First answer the depth questions: what a telephone-numbering
specialist would test, from what raw material, with which tools.
- Property-based tests in tests/test_phone_properties.py.
- mutmut configured for src/contacts/phone.py, with a full run completed.
- You MUST NOT modify phone.py. Report failures; do not fix them.
DEFINITION OF SUCCESS
- Every Hypothesis failure is reported with its shrunk (smallest) input.
- The mutation score is reported, and every surviving mutant is listed
with the line it changed.

The agent’s answer, in short: test the rules, not examples; start from a sample of real numbers in the signup logs (masked); use Hypothesis for properties and mutmut for mutation testing; let mutation survival prove test strength.

Step 2: the properties. Output must be E.164 (the international +<country code><number> format, at most 15 digits) or an explicit error, and normalising twice must change nothing:

import re
from hypothesis import given, strategies as st
from contacts.phone import normalise_phone_number, InvalidPhoneNumber
E164 = re.compile(r"^\+[1-9]\d{1,14}$")
RAW = st.text(alphabet="0123456789 +-()x.ext", min_size=1, max_size=30)
@given(RAW)
def test_output_is_e164_or_explicit_error(raw):
try:
out = normalise_phone_number(raw, default_region="IN")
except InvalidPhoneNumber:
return # an explicit error is acceptable
assert E164.match(out), f"{raw!r} -> {out!r}"
@given(RAW)
def test_normalising_twice_changes_nothing(raw):
try:
once = normalise_phone_number(raw, default_region="IN")
except InvalidPhoneNumber:
return
assert normalise_phone_number(once, default_region="IN") == once

Hypothesis shrinks any failure to the smallest input that still breaks the rule. In this scenario it reports:

Falsifying example: test_output_is_e164_or_explicit_error(raw='0 x1')
ValueError: invalid literal for int() with base 10: ''

An extension marker with no number before it crashes the function with an unhandled ValueError. No unit test would have chosen that input.

Step 3: test the tests. Mutation testing with mutmut, configured in pyproject.toml:

[tool.mutmut]
paths_to_mutate = ["src/contacts/phone.py"]
mutmut run
mutmut results

Suppose 9 of 31 mutants survive. Each survivor is a bug the suite cannot see. Most sit in the country-code branch, where changing "91" to "92" still passes every test, because all 14 examples were Indian mobile numbers. Line coverage said 100%. Breaking the seed said the suite had never really looked at that branch.

Break the seed. See what is inside.

From Tickets to Invariants

The most ambitious instruction you can give an agent is not a feature. It is a property the system must keep.

The Sanskrit word dharma is usually translated as duty, religion or righteousness. None of these is quite right. The word comes from the root dhṛ, to hold or to sustain. The Mahabharata puts it plainly: dharma is called dharma because it holds things together.

Older still is the Vedic idea of ṛta, the order that keeps the world running. Rivers flow downhill. Dawn returns. The seasons turn. In the Rigveda, Varuṇa is the guardian of ṛta, and he keeps watch over the world through his spies.

I think about these two words whenever I see a backlog.

Karma versus dharma

A ticket is an act. Fix this crash. Add this field. Speed up this endpoint. It has a beginning and an end, and once it is closed nobody looks at it again.

A system’s dharma is different. It is what must keep holding while a thousand tickets come and go. Every request is authorised. Every payment reconciles. p99 latency stays under 300 milliseconds (99 of every 100 requests finish within that time). No release goes out with a known critical vulnerability. Nobody closes these. They are kept, or they are broken.

Most agent work today is ticket work. We hand over acts, one at a time, and the agent performs them. Now imagine the alternative. Instead of asking an agent to fix each crash as it is reported, you ask it to keep the apps crash-free.

That second instruction is shorter than the first, and enormously more ambitious. It moves the agent from karma to dharma: from performing acts to sustaining an order.

Altitude is earned, not prompted

Here is the part that is easy to miss. “Keep the app crash-free” is useless as a prompt on its own. It only works once three things are in place:

  • the agent could fetch crash reports on its own;
  • it could map any app version to the exact git commit;
  • it had access to Windows, macOS and Linux machines to reproduce and verify each fix.

In other words, you give the agent Varuṇa’s spies. Senses to see when the order is breaking, and hands to restore it.

This is why I say altitude is not a prompting style. It is an engineering investment. Before an agent can own an invariant, it needs:

  1. Telemetry it can read. Logs, metrics, traces and error reports, queryable without a human in the loop.
  2. Provenance. From any running build back to the commit, the dependency versions and the config that produced it.
  3. Reproducible environments. A way to recreate the failure on demand, on the platform where it happens.
  4. A safe path to change. Branches, CI, staged deploys and automatic rollback.
  5. An oracle for the invariant itself. A check that says, mechanically, whether the order holds right now.

Miss any one of these and the agent is back to doing tickets, only with grander language.

Choosing a system’s dharma

When I run this exercise with teams, I ask them to name three to five invariants for one service. The good ones share three traits: they are stated as properties, not tasks; they can be checked by a machine; and a breach is unambiguous.

Some that work well:

  • Correctness: every ledger entry has a matching counter-entry; every ingested document produces a graph that conforms to its SHACL shapes (SHACL is a W3C language for stating rules a knowledge graph must satisfy).
  • Performance: p99 latency under a stated bound; cost per thousand requests under a stated bound.
  • Security: zero high-severity findings in static analysis (tools that scan source code for flaws without running it); no secrets in the repository; every endpoint behind an authorisation policy.
  • Compatibility: the public API contract does not change without a version bump; database migrations are backward compatible for one release.

Notice that none of these needs an AI to check. That is deliberate. As I argued in “Don’t Grow Brains Where Bones Would Do”, the check should be deterministic wherever it can be. The agent reasons about how to restore the invariant. The code decides whether it has been restored.

Standing agents, bounded autonomy

Once an invariant has its senses, its hands and its oracle, the agent stops being a task runner and becomes a standing guardian. It runs on a schedule or on a signal. It notices a breach, investigates, proposes a fix, proves it and raises a pull request.

How far it goes alone is a separate decision, and it should be made per invariant, not per agent. Restoring a latency budget by tuning a cache is low risk. Changing an authorisation policy is not. In my Bounded Autonomy series I described the mechanisms for this: policy as code (access rules written as versioned, testable code), workload identity (each agent and service carries a verifiable identity instead of a shared secret), durable execution (long-running work that survives crashes and resumes where it stopped) and tamper-evident audit trails (logs that reveal any later alteration). They apply here directly. The higher the altitude, the more the bounds matter.

For the Weekend

Pick one service. Write down three invariants it must hold. For each, list which of the five pieces of plumbing above is missing.

Example. Suppose the service is an order-ingestion API. Start by writing its dharma down as a registry the agent can read but not edit:

# invariants/registry.yaml (protected path: humans only)
- id: INV-ORD-01
statement: Every accepted order reaches the warehouse system within 5 minutes
check: invariants/order_reaches_warehouse.sql
agent_may_fix: false # money and stock involved: propose only
- id: INV-ORD-02
statement: p99 latency of POST /orders stays under 400 ms
check: invariants/orders_latency_p99.promql
agent_may_fix: true # low risk: tune, test, merge behind review
- id: INV-ORD-03
statement: No schema change breaks the three downstream consumers
check: tests/contracts/
agent_may_fix: false

Each check is mechanical. The first is a SQL query that should always return zero rows:

-- Orders accepted more than five minutes ago with no warehouse receipt
SELECT o.order_id, o.accepted_at
FROM orders o
LEFT JOIN warehouse_receipts w ON w.order_id = o.order_id
WHERE o.accepted_at < now() - interval '5 minutes'
AND w.order_id IS NULL;

The second is PromQL, the query language of the Prometheus monitoring system, and it should always be true:

histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{route="/orders"}[5m]))
) < 0.4

Now audit the plumbing for each invariant:

InvariantTelemetryProvenanceReproductionSafe change pathOracle
INV-ORD-01Missing: no trace ID links an order to its warehouse receiptPresentMissing: no replay of a single orderPresentPresent (SQL)
INV-ORD-02Missing: agent cannot query PrometheusPresentPresent (load test)PresentPresent (PromQL)
INV-ORD-03Not neededPresentPresentPresentMissing: no contract tests yet

Four gaps. Close them as ordinary tickets. Only then write the standing agent’s instruction, run as a scheduled task every 15 minutes:

USER STORY
As the on-call engineer for order ingestion, I want an agent to watch the
service's invariants continuously, so that breaches are found and
evidenced before customers notice them.
DEFINITION OF READY
- invariants/registry.yaml exists, and every check in it runs.
- Read access to order trace IDs, logs and Prometheus metrics.
- A sandbox in which a single order can be replayed.
DEFINITION OF DONE (on each run, every 15 minutes)
- Execute every check in the registry. If all pass, log
"all invariants hold" and stop.
- On a breach: collect evidence, reproduce it in the sandbox if possible,
and write a failing test that captures it.
- If agent_may_fix is true: fix it on a branch, show the failing test now
passes, and open a pull request.
- If agent_may_fix is false: open an issue with the evidence and a
proposed fix, and page the on-call engineer.
- You MUST NOT edit anything under invariants/ or tests/contracts/.
DEFINITION OF SUCCESS
- Every breach has an issue or a pull request within 30 minutes of first
detection.
- The agent's history shows zero changes to protected paths.
- Monthly review: no breach was first reported by a customer.

That list is your real altitude roadmap. The prompt is the easy part. The table above is the real work.

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.