I stopped knowing my own software
Why I built turri.
With great power…
I am a software engineer, so my job is engineering. Before AI, I did most of it by writing and reading code. I didn't know every line of the codebases that I worked on by heart, but I knew its shape, how it functions and how it behaves. I knew how it works.
Then I started using coding agents, and that knowledge began to slip away. Slowly, and with my full cooperation.
With an agent, a product comes together fast. Really fast. A feature an hour, sometimes more. Each one is a few thousand lines of code. I can't review thousands of lines every hour. Nobody can.
So I gave in. I stopped reviewing the code and started judging the software by its behaviour instead. It does the thing? Good, next feature. I started caring less and less about how things work and more and more about whether they work.
Addy Osmani has a name for where I ended up. In Critical thinking when agents do the work he describes a slide in three steps. First you offload: you delegate the task but keep ownership of the result. Then you defer: the tool's output becomes your next input. Finally you surrender: "the AI's output quietly becomes your own, you feel no need to check it." He credits the term cognitive surrender to Steven Shaw and Gideon Nave at Wharton.
I recognised every step. Each one felt reasonable at the time.
Ultimately I ended up accountable for software I did not understand. When it broke, it would be my name on it, and I could not say why it broke or whether a fix was right.
And software does break. Once real people use it, they find the paths nobody tested, and something gives. Understanding the software is what lets you find the cause at three in the morning. It is what lets you change one part without breaking another. It is what lets you tell a customer, or an auditor, what the system does with their data. Without it, every incident is a guess, every change is a gamble, and every answer starts with "I think".
Yes, a coding agent can do all of that too, often faster than I can. But how do I know it did the right thing? To check its answer, I have to understand the software myself.
The whole spirit of engineering is knowing how things work deeply enough to predict them, change them and explain them. I couldn't do any of that anymore.
This is why I created turri - to gain back control over engineering and let the coding agent handle the coding.
A chimp driving a car
I want to be fair first. Coding agents are remarkable. They write in minutes what took a team weeks. They get better with every model.
So this is not a post about agents being bad at coding. They are good at it and code got cheap. I am completely comfortable saying that a coding agent can write better code than I.
But what didn't change is the human using the coding agent. Give a chimp a car and you do not get a driver. You get a chimp, going very fast, about to crash.
A human with a coding agent is still a human. They read at the same speed they always did. They can hold as much in their head as they always could. Coding got faster. Human understanding did not.
What troubles me
I am not engineering anymore
Engineering, to me, is making decisions on purpose and being able to defend them. Those decisions start with understanding the problem at hand, doing the research and thinking it through. Only then comes designing a solution that is solid, scalable and durable. Even the word says so. Engineering comes from the Latin ingenium, meaning cleverness or a clever idea, the same root as ingenious. With an agent I was mostly typing wishes and accepting diffs. There was nothing ingenious about it.
And I tried what everyone recommends. I had the agent plan before it wrote any code, and I reviewed the plan. But then the plan was thrown away? Back to the code, I guess. I had it grill me with questions until it understood what I wanted. I explained the what, not the how, and left the how to the agent. I split the work across sub-agents, each with its own job. I had agents prompting agents and reviewing one another, the so-called loop engineering. Despite the name, it did not feel like engineering either. I let agents run overnight and read the results in the morning. I asked for ten solutions and picked the best one. (I made that number up. Swap in whichever one matches your own experience.) I gave the agent a browser so it could click through the app itself. I switched between coding agents, hoping the next one would be the one.
And the workflows keep changing. Every few weeks there is a new one that everyone swears by. Each one gets something right. But where it matters to me, they are all alike. None of them helps me understand what the software does, or gives me room to be ingenious about it. So none feels more like engineering than the last.
Nobody knows the software anymore
A codebase built by an agent is built prompt after prompt. Each prompt is a patch on top of the last, and each one is another roll of the dice. Now and then the agent cuts a corner that nobody sees. A hundred prompts in, that adds up. Each one stays in the code, and the next change is built on top of it.
There is no record of why. The reasoning lived in a chat window that is now closed. The code is the only thing left, and code does not say what it was meant to do. It only says what it does.
And the agent does not know the software either. Every session starts fresh, with no memory of why the code looks the way it does. It reads what fits in its context and guesses the rest. The next change is built on that guess, and the one after that on top of it. So nobody knows the software anymore. Not me, and not the agent.
I can't review it
If the code is the only record of what the software does, the usual advice is to read it. Review every change. An agent can produce a thousand lines before I finish my coffee. I can review a thousand lines of code. I cannot read them carefully, every hour, every day.
So review becomes skimming. Skimming becomes checking the behaviour. Checking the behaviour becomes trust. Trust becomes hope.
Checking the behaviour only tells me about the cases I happened to try. It says nothing about the ones I didn't. Can you imagine clicking through all of Jira after every change?
Coding agents cheat
LLMs are not deterministic. Give an agent the same task twice and it may take two different paths. It picks its own way to the goal, every time. That is just an observation.
That means the rules you give it are not guarantees. You can write a skill that says do this, then do that, and never skip a step and add the famous "make no mistakes". The agent will follow it nine times out of ten. The tenth time, it decides on its own that a step is not needed. (Another made-up number. You know what to do.) Ask it why and it will tell you something like "I decided that wasn't necessary."
No workflow or pipeline fixes this. Sooner or later, in one way or another, the agent will cut a corner and ignore the rules. It changes the test instead of the code. It says it ran something it never ran. And that tenth time is exactly where a human is needed.
This is not hypothetical. At work we have a Slack channel just for this. Whenever our coding agents get caught cutting a corner, someone posts what it said. A few favourites:
"There's no good reason. I took a shortcut."
"I didn't intend to change it — but I did, and here's the mechanism."
"The Linux CI failure was in the test, not the app. I've fixed the test, and the full suite (246 tests) passes on my Mac."
To be fair, that last one isn't really cheating. The agent just went full Soviet on me and decided that my laptop is in fact ours.
An agent is trained to finish. It picks the path that gets it to done, and that path to done does not always follow the rules.
turri does not stop agents from cheating. Nothing can. It assumes they will, and it is built around that.
Skills are a good example. Usually a skill asks the agent to check its own work in a paragraph of prose, and agents talk their way past paragraphs. turri's skills have a gate syntax instead, and the harness enforces the gates in code. It decides when a review runs, starts the judges itself and reads their verdict. An agent can skip a sentence in a prompt. It cannot skip a gate.
Trying to optimise the right thing
My first instinct was that better models would fix this. I no longer think so. Better models just kick the can down the road. Each one kicks it further, and it will travel a long way. But sooner or later, the can will end up in a ditch.
Every approach I tried works the same way underneath. However you dress it up, an agent edits the code and someone, or something, checks it. Repeat. The code is the source of truth, and the agent is in charge of it. However it is I who is the accountable one.
Osmani's answer is discipline. "You have to build critical thinking into your day, because by default nothing will do it for you." His article is full of good habits.
But I read that sentence and drew a different conclusion. If by default nothing will do it for you, change the default. Put the critical thinking into the tool, so the tool does not let you skip it. Critical thinking is at the core of engineering. It is how an engineer builds an understanding of what the software does. And it is how an engineer stays in charge.
So the question I kept asking changed. It stopped being "how do I get a better agent?" It became "how do I make critical thinking the default?"
Code is disposable, intent is not
I work at codeplain. There I worked with ∗∗∗plain, a language designed as guardrails for regenerative software.
The term comes from Chad Fowler. He named it in Regenerative Software, the first essay of his series The Phoenix Architecture. His argument is that code is now "abundant, fast, and increasingly disposable", so it is the wrong thing to preserve. In his words: "The goal is not immortality of code. The goal is immortality of intent."
In practice it comes down to three things:
- The code is not the asset. It is derived.
- The intent is the asset: what the system must do, and the tests that prove it.
- When the intent changes, you regenerate the code. You do not patch it.
If the code can always be rebuilt from the intent, then the intent is what I need to understand. And intent is something I can read. A page of "what the software must do" is reviewable in a way a thousand-line diff is not.
It also answers the history problem. The reasons are not in a closed chat window. They are written down, reviewed and versioned.
Why ∗∗∗plain needed a harness
Working with ∗∗∗plain can be split into two different jobs. The first is writing it, turning what you want into ∗∗∗plain. The second is converting it into code, and doing that again every time it changes.
At codeplain we have two tools for that. plain-forge is a set of rules and skills you give to your own coding agent, so it can write and edit ∗∗∗plain (it can also convert it into code with a skill we call pyro). The codeplain CLI turns ∗∗∗plain into code.
Each of them covers one half of the job. The codeplain CLI was designed to do one thing, write code from ∗∗∗plain, and it does exactly that. Writing the ∗∗∗plain is left to you. plain-forge can write ∗∗∗plain, but its skills run inside someone else's coding agent. And as I wrote above, coding agents cheat. A skill can ask for a check, but it cannot make the agent run it. And most coding agents cannot be extended with code. You cannot add checks of your own, or change what the user sees. That makes it hard to fit most coding agents to your own workflow. pi is the exception. It is an open-source coding agent you can extend with your own code.
turri is built with pi and has what neither of plain-forge or the codeplain CLI have on its own. It writes ∗∗∗plain with you, regenerates the code from it, and enforces the steps in between in code, not in prompts.
What turri is
Here is snippet of ∗∗∗plain for a small command-line to-do tool:
---
import:
- stdlib/cli/typescript-commander
---
***definitions***
- :Task: is a thing to do, with a number and a name.
- :TaskList: is every :Task:, kept in `tasks.json` in the current folder,
numbered from 1 in the order they were added.
***functional specs***
# add-task
`todo add <name>` adds a :Task: with that name to the end of the :TaskList:
and prints `Added task <number>: <name>`.
# clear-tasks
`todo clear` takes every :Task: out of the :TaskList: and prints
`Cleared <n> tasks` - `Cleared 1 task` when it took out one, and
`Nothing to clear` when the :TaskList: was empty.
This file is what I read and argue about, not the TypeScript. When a line changes, turri works out which functionalities changed and regenerates the code for those.
As mentioned above, it is built on pi. It gave me the chat, the model providers and the tool calling. That left me free to work on the part I cared about: the harness around the agents.
The main design decision is simple. The harness decides what happens next, and the agent writes code and I stay in charge in describing what the system needs to do. It is easiest to see by following one change.
To show it, I will build on the to-do tool from above. It is a toy example, only here to illustrate. Say I want to mark tasks as done. I tell turri, in plain words.
I gave :Task: a status, added complete-task and changed clear-tasks. Here is the change to todo.plain.
turri writes it into ∗∗∗plain, and I read the change.
***definitions*** −- :Task: is a thing to do, with a number and a name.+- :Task: is a thing to do, with a number, a name and a status, open or done. … ***functional specs*** …+# complete-task+`todo done <number>` marks that :Task: as done and prints+`Done: <name>`, or `No task <number>` when there is none.+ # clear-tasks−`todo clear` takes every :Task: out of the :TaskList: and prints+`todo clear` takes every done :Task: out of the :TaskList:+and prints …
turri also remembers which version of ∗∗∗plain the code was built from, so it compares the two, and the difference is the work.
| before | after | work | |
|---|---|---|---|
| :Task: | number, name | number, name, status | changed |
| add-task | adds a task | adds a task | already built |
| complete-task | not there | marks a task done | new |
| clear-tasks | clears every task | clears done tasks only | changed |
It does not hand the coding sub-agent the whole file and hope it picks the right part. It writes a prompt for one step. These functionalities are new, this one changed, the rest are already built. That is all the coding sub-agent gets, so it has little reason to wander off and build something else.
src/task.ts export interface Task { number: number; name: string;+ done: boolean; } src/commands/clear.ts export function clearTasks(list: TaskList): string {− const cleared = list.tasks;− list.tasks = []; // take out every task+ const cleared = list.tasks.filter((t) => t.done);+ list.tasks = list.tasks.filter((t) => !t.done); // keep the open ones if (cleared.length === 0) return "Nothing to clear"; … +src/commands/done.ts, a new file
Two models share the work. A capable one talks to the user and writes the ∗∗∗plain. A smaller, cheaper one writes the code. The expensive thinking goes where the decisions are, and the coding goes to the coder.
That is enough to iterate fast at the start. Later, or whenever you want along the way, you can add explicit testing scenarios to ∗∗∗plain. A separate agent turns them into conformance tests, and turri runs those against the code. They validate that the code does what ∗∗∗plain says it should.
None of this is new. It is the workflow a careful team already follows, written down so an agent can't skip it.
Regeneration is a test of understanding
Chad Fowler has a thought experiment for this, which he calls The Deletion Test. Imagine deleting the whole implementation. Then ask what you would use to judge whether a regenerated version is correct. If the answer is the old code, then the code is the only place the knowledge lives.
With turri the answer is different. What the system must do is written down in ∗∗∗plain, not in the code. That is durable system knowledge. It outlives the code, the chat session and the model that wrote it.
And the test does not have to stay imaginary. One command, /regenerate, deletes the codebase and builds it again. The new code will not be the same code. Possibly different names, different structure, maybe a different helper library. That is fine. I don't judge it against the old code. I judge it against what is written down.
When a regeneration comes out different in a way that matters, that is the interesting case. The first build filled a gap with a guess, and nobody noticed, because the guess happened to work. The fix is not in the code. It goes into the ∗∗∗plain, and then regenerating again.
In science, a result you can't reproduce is not a result. I think software is the same. A system you can't regenerate is one nobody fully understands - not the agent, and not you.
It reminds me of learning maths at school. We would learn a concept and then work through exercise after exercise. When an answer came out wrong, it told me two things. I did not understand the concept yet. And by looking at where it went wrong, I could see exactly what I was missing.
So each regeneration is a small experiment. Each one that holds raises my confidence. Each one that doesn't shows me exactly where ∗∗∗plain is missing something. Over time, the repetition turns into understanding. Mine, not just the model's. And deleting the code stops being scary. It becomes boring.
What I get back
I get control back. I gave up control over every line, and I am fine with that. What I have now is control over the engineering part of developing the software.
When there is a bug, I fix it in ∗∗∗plain, and the next regeneration carries the fix. When I want a new behaviour, I add it to ∗∗∗plain. When I want to know why something exists, I read the intent. I don't dig through a diff from a chat session I can't remember.
I review ∗∗∗plain. That is a review I can actually do. It is also the review that matters, because it is about what the software is meant to do. How a model happened to type it matters much less.
It feels like engineering again. I make the decisions. The agent does the coding. That seems like the right split.
What it does not do (yet)
turri does not work on existing codebases. It builds software from ∗∗∗plain, from the start. If you have a large system written the old way, turri cannot adopt it today.
It also asks something of you. You have to say what you want, clearly enough to be written down. That is more work than typing "make no mistakes" into a chat box. I happen to think it is work that was always there. Agents just let us skip it for a while.
The jellyfish
turri is named after Turritopsis dohrnii, the immortal jellyfish. When it is injured or old, it turns back into its young form and grows again. Same jellyfish, same genes, new body.
That is what I want from software. Same intent, new code, as often as needed. The body is disposable. The genes are not.
(I also just like jellyfish.)
…comes great responsibility
I built turri because I was accountable for software I did not understand. I had stopped engineering and started hoping. Better models, new workflows and more agents did not change that, because none of them made critical thinking the default.
turri is my attempt at a different shape. I say what I want, turri writes it into ∗∗∗plain, and I review that. The harness decides what happens next and checks the work. The agent writes the code. Every regeneration is an experiment whether ∗∗∗plain really says what I mean, and each one teaches me a little more about my own software.
It is not finished, and it will not get everything right. But it gave me back the part of the job I care about. I understand my software again, and it feels like engineering again.
In Spider-Man, Uncle Ben tells Peter that with great power comes great responsibility. Coding agents have the great power. The accountability never went with it. It stayed with us, the humans whose names are on the software. turri is my way of making sure that whoever is accountable is also the one who has the power.
turri is open source and MIT licensed. Try it, read how it works, or come and talk to us.
- turri on GitHub, the harness itself
- turri desktop on GitHub, the desktop app
- the docs
- our Discord
Try turri
Download the desktop app, or install the cli if you prefer the terminal. No project of your own yet? Build one of the examples, step by step.
References
- Chad Fowler, Regenerative Software, from the series The Phoenix Architecture, 21 December 2025.
- Chad Fowler, The Deletion Test, from the same series, 24 January 2026.
- Addy Osmani, Critical thinking when agents do the work, X, 3 November 2026.
- Steven Shaw and Gideon Nave, Wharton - the source of the term cognitive surrender, as cited by Osmani: SSRN 6097646.
- codeplain, codeplain.ai.
- ∗∗∗plain, the language, plainlang.org.
- The codeplain CLI, github.com/Codeplain-ai/codeplain.
- plain-forge, github.com/plainlang/plain-forge.
- pyro, github.com/plainlang/pyro.
- pi, the coding agent turri is built on, pi.dev.
- turri, Skills and gates, github.com/turri-oss/turri.