六个月只用 Agent 写代码
英文标题:🔶 Six months of writing code exclusively with agents
作者从今年 2 月起规定自己不再手写代码,所有开发任务都必须由 AI Agent 完成;当 Agent 卡住时,他必须改进提示词、工具或环境,而不是亲自补完代码。随着 Claude Code、GPT-5.3 和 Opus 4.6 等模型能力提升,他逐步从单 Agent 转向并行运行多个隔离 VM,并构建了名为 `botd` 的系统统一管理任务、对话和代码差异。在高峰期,他同时运行约 20 台 VM,但发现测试通过、截图正常和 Agent 审查通过,并不等于某项功能值得合并。文章的核心结论是:Agent 让交付代码变得更容易,但产品判断、风险控制、代码理解与决定“该不该发布”反而变得更加重要。
6.9 KB
67.4 KB
79.6 KB
300.4 KB
45.2 KB
99.3 KB
中文
今年 2 月,我给自己定了一条规则:不再亲手写代码。六个月来,我一直遵守这条规则。
系统曾经存在于我的脑海中
回到 2024 年、AI 出现之前,我的超能力是了解整个系统如何运作,尤其是不同组件之间的接口。
如果有人来找我,说他们想实现一个功能,或者正在修复一个 Bug,我通常能直接指出关键代码的准确行号,并告诉他们需要改什么。我也记得那些看起来奇怪的决策为何存在,以及哪些假设从未被写下来。
这些知识是我花了数月乃至数年在代码库中工作积累而来的。它来之不易,且无比珍贵。它让我能快速构建功能,更重要的是,能够安全地构建功能。
代价是,我必须跟上所有事情。随着越来越多的人参与贡献,我花在阅读变更、仅仅为了维持这一心智模型上的时间越来越多。
更大的代价是打字。每当我想构建什么东西时,我脑中都能看到代码,只是无法足够快地把它打出来。
打字速度只是问题的一部分:一个功能几乎从来不只是一次编辑。即便很小的改动,也会跨越多个层次,涉及 handler、schema、测试和文档。而且这些编辑并不等价:错误的 handler 可以回滚,但错误的 migration 可能会留下一片混乱。因此,亲手写代码意味着要安全地把一个决策贯彻到它所影响的每一个位置。
Copilot 的自动补全立刻带来了帮助:一条文档注释会变成初稿,通常不对,但总比面对空白文件再开始编辑好。Cursor 的 Tab 补全帮助更大。模型显然正在快速进步。
Claude Code 改变了很多事。我只需描述一次改动,Agent 就能同时编辑一批文件。
因此,我打的字少了很多。但少打字不等于少工作:我会阅读模型生成的每一处变更,并将它与我脑中预期的目标状态逐一核对。Agent 仍然经常出错,也会做出不必要的改动。增量式工作能让它们保持在正轨上。这意味着我会手动编辑部分生成的代码。毕竟,我仍要为每一行被合并的代码负责,模型不会被追责。
随后在今年年初,模型几乎一下子变得非常出色。GPT-5.3 和 Opus 4.6 突然可以处理更大的改动,所需的引导少得多,产出终于好到足以继续在其基础上构建。
所以在 2 月,我定下规则:不再亲手写代码。如果 Agent 卡住了,我不能自己把代码写完。我必须弄清 Agent 缺少什么,并修复那个问题。
我不是靠阅读关于编程的文章而变得擅长编程的。我是靠写大量代码、运行它、看着它失败、修复它,然后再重复这一过程而变得擅长的。
毕竟,AI Agent 也只是软件。我不会靠阅读提示词指南来理解它们。我必须把它们用于真实工作,观察它们在哪里失败,修改提示词、工具或环境,然后再试一次。
这条规则迫使我完成了那些反复练习。
我有一次违反了规则,持续了三分钟。我打开代码,写了几行,感觉非常好。我一直很怀念这种感觉。直到我意识到还得继续打多少字,我立刻放弃了。
一个 Agent 变成了一打
当我不再亲自输入代码后,我开始发现一些空闲时间。我给一个 Agent 分配任务,而它工作时我无事可做。
我没有等待,而是再启动一个 Agent 去做别的事。然后又这样做了一次。
我并非有意在构建一个并行系统。我只是填补任务之间的时间。我有 ADHD,我分心了。
很容易想象,如果你和同事共用一台开发机,会发生什么。现在再想象一下:他们彼此不交流,而且同时都在工作。
那就是我的第一个并行配置。Agent 会改动相同文件和 Git 状态,安装依赖,争抢端口,并留下仍在运行的进程。我还得协调每个 Agent 何时能够测试、推送或部署。更糟的是,其他 Agent 往往无法继续推进,直到运行时间最长的那个 Agent 完成。我启动更多 Agent 是为了避免等待,却不知怎么又创造了一种新的等待方式。
我询问朋友和同事,他们如何处理这件事,结果每个人都有自己的变通方案。
最先被提到的是 worktree。每个 Agent 都有自己的 checkout 和分支,源代码冲突大多消失了,但 worktree 只解决了 Git 的问题。Agent 仍共享数据库、端口、进程以及机器上的其他资源。
因此,人们用 AGENTS.md 来打补丁:使用随机端口,创建临时数据库,不要碰其他 Agent 的进程。每一次冲突都会变成一条额外指令,而 Agent 消耗上下文,是为了弄清如何不互相踩脚,而不是完成任务。
容器更接近目标:端口、进程和本地状态彼此隔离。但边界并不严密:我的笔记本电脑能够访问的任何东西,容器理论上也可能访问。错误命令的爆炸半径没有被限制,因此我仍然需要批准命令。最糟糕的是,我的笔记本必须保持唤醒状态。如果我把它合上,所有工作都会停止。
我合上了笔记本,工作仍在继续。
到这个阶段,我加入了 exe.dev。我们提供可在几秒内启动的 Linux VM,并且 SSH 和 HTTPS 均已预先配置好,因此把 Agent 从笔记本迁走自然是下一步。
每个任务都有自己的机器。我可以合上笔记本,离开,而工作仍会继续运行。
但现在我有了一个新问题:如何可靠地启动一个完整的开发环境,供 Agent 处理我希望它处理的任何项目?

于是,秉持不写代码的原则,我让 Claude 编写一个启动脚本。我告诉它我想要什么,并要求它循环执行,直到一切正常。它安装我们的工具链、克隆仓库、配置 Claude Code 和 Codex,并完成把一台全新 VM 转变为开发环境所需的一切。接着,它运行验证循环:启动一台新机器,运行脚本,看看哪里出错,修复脚本,然后再试一次。
这些 Agent 机器可以工作了,但每个 Agent 仍各自拥有一个 tmux 会话。为了查看每个 Agent 在做什么,我最终不得不一直打开一打终端窗口。我需要在它们之间跳转,找出哪个 Agent 完成了、哪个卡住了、哪个需要我提供信息。我需要一个地方来管理它们所有。
于是我构建了 botd。
我给它定了三条规则。第一,它必须运行在笔记本以外的地方(这样我合上笔记本后 Agent 仍能继续工作)。第二,移动端必须是一等公民。管理 Agent 不应该要求人坐在终端前。第三,它必须保存每段对话,让我能够跨 Agent 回溯查看,理解它们在哪里卡住、哪些指令有效,以及哪些问题反复出现。
botd 会创建和销毁 Agent 机器,在底层驱动 Agent,并跟踪每一个任务。它向我展示哪些 Agent 正在工作、哪些卡住了、哪些正在等待我。通过手机或笔记本,我可以查看对话、发送后续指令,并审阅 diff。
我不再需要管理一打终端会话,而是有了一个统一管理工作的地方。
如果我必须批准每一次工具调用,这一切都无法成立。那只会让我再次变成队列瓶颈。每个 Agent 都在隔离、可销毁的 VM 中运行,所以我让它以 YOLO 模式运行。它可以执行 bash 命令、安装软件包、启动服务,并按需更改任何东西。即使环境被弄坏,对我来说也只损失一台 VM。
但一个只能触及自身 VM 的 Agent 并没有多大意思。我仍希望 Agent 能读取日志、从 Git 拉取、调用 Anthropic 或 OpenAI,以及检查 Stripe 中的内容。
这些访问权限才是真正的风险所在,而 VM 无法限制它。Agent 会读取不受信任的内容,并可能因此受到提示词注入;它能访问到的任何东西,被注入后的 Agent 都可能泄露或破坏。我知道自己还有一些漏洞尚未填补。因此,每一项外部访问权限都会面对同一个问题:通过它最坏会发生什么?读取大多可以通过:Agent 被授予只读权限。写入则不行,所以写入权限被限制在测试环境中,最坏情况只是测试数据损坏。
我也不希望凭据存放在 VM 内。通过 exe.dev integrations,Agent 经由代理发送请求,代理添加凭据,而 Agent 在从未见到秘密信息的情况下接收响应。
一切都通过了,但我仍然不想要它。
高峰时,我同时运行大约 20 台 VM。并非所有 VM 都处于活跃状态。有些任务启动后搁置数周,最终被放弃,因为认知负担太高,而这些工作又始终不够紧急或不够重要,无法持续推进。
在这样的规模下,验证很糟糕。我无法手动重建每个分支、重新运行其测试,再亲自检查应用程序。但在各自隔离的机器中,Agent 可以运行测试、触发完整 CI 套件、启动应用程序,并通过浏览器操作它们。它们会给我发送已完成工作的截图。
但 Agent 仍在给自己的工作评分。如果它误解了我的需求,它可能构建了错误的东西,为错误的东西写测试,然后自信地告诉我一切都通过了。
这就是为什么能够打开正在运行的环境很重要。我可以亲自使用它,从头到尾操作新的 UI,并确认它按我想要的方式工作,而不只是按 Agent 所说的方式工作。
然后还有审查问题。当我亲自写代码时,到准备审查时,我已经理解了这项变更。毕竟,我在过程中做出了所有决策。使用 Agent 后,整个 diff 会一次性出现。它可能测试通过、截图正常,但对我来说仍是陌生代码。当多个 Agent 差不多同时完成时,我面前会出现一条完整变更队列,在合并任何一项之前,我都得先把它们装进脑子里理解。

让其他 Agent 审查代码的效果出乎意料地好。它们偶尔能发现真实 Bug,而且运行多次审查的成本足够低。但我不能仅仅因为 Agent 批准了某个东西就合并它。我仍然必须理解这项变更。我仍然要为被合并的代码负责。
有时,我确实理解了它。测试是绿的,截图看起来不错,UI 完成了我要求的事情,就连 schema 看起来也不错,而且代码获得了所有审查 Agent 的批准。

但我还是把它扔掉了。
也许根本没人需要它。也许它为我们已经支持的某件事引入了第二种做法。也许一个小便利增加了我们要多年背负的复杂性。
工具可以告诉我这项改动是否有效,但它们无法告诉我是否值得把它加入系统。
取消上线某个东西比发布它困难得多。Hyrum 定律会开始生效:一旦足够多的人使用一个系统,就会有人依赖每一种可观察到的行为,即使那些行为从未被你打算作为契约。移除某个东西会破坏你不知道存在的用户、脚本和工作流。
发布变得容易了。决定发布什么变得重要了。
并非所有 Agent 都写代码
以上内容都与交付代码有关。但我们运行的一些最有用的 Agent根本不交付代码。
值得暂停一下,看看 Agent 实际上是什么,因为这个词听起来比它应有的分量更重。Agent 是一个带工具的、运行在循环中的模型。向模型发送一条消息;如果它请求工具调用,就运行工具并把结果发回;重复。这就是全部。
模型让这个循环具备能力。给它一台真实计算机上的 bash,它就可以安装缺失的东西,在你的 grep 使用不同参数时适应变化,并持续推进直到工作完成。
循环本身从不改变。工具决定 Agent 能成为什么。开发 Agent 需要一台完整的计算机:shell、编译器、浏览器,以及安装各种东西的自由。限制这些,Agent 就毫无用处,你又会回到批准每一次操作的状态。
我的起点是:给 Agent 我会给开发者的一切。优秀的开发者工具已经被证明也是优秀的 Agent 工具。最好的 Agent 工具最终是否还会是开发者工具,时间会给出答案。
真正需要害怕的不是任何单一工具,而是组合。私有数据、不受信任的内容、外部通信:其中任意两项都可控。但把三项都放进一个 Agent,就是让你的秘密自己走出大门。这正是 Simon Willison 所说的致命三要素。所以我并不盲目地最小化工具数量:我隔离环境,并关注各种组合。
这是第一个例子:调查。当客户报告问题时,我有一条会使用的提示词,它简单得令人尴尬:
“客户报告:<他们的报告,原文照录>。请使用 ClickHouse 日志查明发生了什么。”
原文照录很重要。如果我总结客户报告,Agent 就会继承我的解读——以及我的盲点。给它客户自己的原话后,它会查询日志、阅读相关代码,并重建实际发生了什么。
然后决策权在我手中。有时我会要求它给出选项,再从中挑一个。有时证据表明系统行为符合预期,修复方式是文档或邮件,而不是代码。无论哪种情况,我都是基于证据做决定,而不是根据一份 Bug 报告猜测。
第二项工作:攻击。我们的红队 Agent 只有一条指令:尝试侵入我们的系统。
English / 原文
In February of this year, I made a rule for myself: I wasn’t going to write code by hand anymore. I’ve been living by that rule for six months.
The system lived in my head
Back in 2024, before AI, my superpower was knowing how the entire system worked, especially the interfaces between its different components.
If someone came to me with a feature they wanted to build or a bug they were trying to fix, I could usually point them to the exact line of code that mattered and tell them what needed to change. I also remembered why the strange-looking decisions existed and which assumptions were never written down.
This was knowledge I’d built over months and years of working in the codebase. It was hard-earned and invaluable. It let me build features quickly and, more importantly, safely.
The cost was that I had to keep up with everything. As more people contributed, I spent more and more time reading changes just to maintain that mental model.
The bigger cost was the typing. Every time I wanted to build something, I could see the code in my head. I just couldn’t type it out fast enough.
Typing speed was only part of the problem: a feature was almost never one edit. Even a small change spanned multiple layers and touched handlers, schema, tests, and docs. And those edits weren’t equal: a bad handler could be reverted, but a bad migration could leave a mess behind. So writing the code by hand meant carrying one decision safely through every place it touched.
Copilot autocomplete helped immediately: a doc comment became a first draft, often wrong, but beats editing a blank file. Cursor’s tab complete helped more. The models were clearly improving fast.
Claude Code changed a ton. I could describe the change once, and the agent would edit a bunch of files at once.
As a result, I typed a lot less. But typing less didn’t mean working less: I read every change the model generated to match it against the desired state I had in my head. Agents would still be wrong quite a lot and make changes that weren’t required. Working incrementally kept them on track. This meant hand editing some of the generated code. After all, I was still responsible for every line that merged. The model wasn’t going to be held accountable.
Then, early this year, the models got really good, almost all at once. GPT-5.3 and Opus 4.6 could suddenly handle larger changes with much less steering, and the results were finally good enough to build on.
So in February I made the rule: no more code by hand. If an agent got stuck, I wasn’t allowed to finish the code myself. I had to figure out what the agent was missing - and fix that instead.
I didn’t get good at coding by reading about coding. I got good by writing a lot of code, running it, seeing it fail, fixing it, and doing it again.
AI agents are just software, after all. I wasn’t going to understand them by reading prompt guides. I had to use them for real work, see where they failed, change the prompts, tools, or environment, and try again.
The rule forced me to get those reps.
I broke it once, for three minutes. I opened the code and wrote a few lines, and it felt great. I had missed this. Right up until I realized how much I still had to type. I noped out.
One agent became a dozen
Once I stopped typing the code myself, I started finding these pockets of free time. I would give an agent a task, and then there was nothing for me to do while it worked.
Instead of waiting, I spun up another agent to do something else. Then I did it again.
I wasn’t intentionally building a parallel system. I was just filling the time between tasks. I have ADHD. I got distracted.
It’s easy to imagine what would happen if you shared a single dev box between colleagues. Now imagine they don’t talk to each other and they’re all working at the same time.
That was my first parallel setup. The agents changed the same files and Git state, installed dependencies, fought over ports, and left processes running. I also had to coordinate when each agent could test, push, or deploy. Worse, I often ended up waiting for the longest-running agent before the others could move forward. I had started more agents to avoid waiting and somehow created a new way to wait.
I asked friends and colleagues how they dealt with this, and everyone had a workaround.
Worktrees came up first. Each agent got its own checkout and branch, and the source collisions mostly went away, but worktrees only solved the Git part. The agents still shared databases, ports, processes, and the rest of the machine.
So people patched around that with AGENTS.md: use a random port, create an ephemeral database, don’t touch another agent’s process. Every conflict became another instruction, and the agents burned context figuring out how not to step on each other instead of doing the task.
Containers got closer: separate ports, processes, and local state. But the boundary was leaky: whatever my laptop could reach, the container could potentially reach too. The blast radius of a bad command wasn’t contained, so I was still approving commands. And worst of all, my laptop had to stay awake. If I closed it, all of the work stopped.
I closed my laptop. The work kept going.
By this point, I had joined exe.dev. We make Linux VMs that come up in a couple of seconds with SSH and HTTPS already set up, so moving the agents off my laptop was the natural next step.
Each task got its own machine. I could close my laptop, walk away, and the work kept running.
But now I had a new problem: how do I reliably bring up a complete development environment for whatever I want the agent to work on?

So, staying true to not writing code, I asked Claude to write a startup script. I told it what I wanted and instructed it to loop until things worked. It installed our toolchains, cloned the repositories, configured Claude Code and Codex, and did everything needed to turn a fresh VM into a development environment. Then it ran the validation loop: bring up a fresh box, run the script, see what broke, fix the script, and try again.
The agent boxes worked, but each agent still had its own tmux session. I ended up keeping a dozen terminal windows open just to see what each agent was doing. I had to jump between them to find which agent had finished, which one was stuck, and which one needed something from me. I needed one place to manage all of them.
So I built botd.
I gave it three rules. First, it had to run somewhere other than my laptop (the agents should keep working when I closed it). Second, mobile had to be first-class. Managing agents shouldn’t require sitting in front of a terminal. Third, it had to preserve every conversation so I could look back across agents and understand where they got stuck, which instructions worked, and which problems kept repeating.
botd provisioned and deprovisioned the agent boxes, drove the agents underneath, and kept track of every task. It showed me which agents were working, which were stuck, and which were waiting for me. From my phone or laptop, I could inspect the conversation, send follow-up instructions, and review the diff.
Instead of managing a dozen terminal sessions, I had one place to manage the work.
None of this worked if I had to approve every tool call. That would just turn me into the queue again. Each agent was running inside an isolated, disposable VM, so I let it run in YOLO mode. It could run bash commands, install packages, start services, and change whatever it needed. A trashed environment cost me nothing but the VM.
But an agent that can only touch its own VM isn’t very interesting. I still wanted agents to read logs, pull from Git, call Anthropic or OpenAI, and inspect things in Stripe.
That access is where the real risk lived, and the VM did nothing to bound it. An agent reads untrusted content and can be prompt-injected by it; whatever it can reach, an injected agent can leak or corrupt. There were holes I knew I hadn’t filled. So every piece of external access got the same question: what’s the worst that can happen through this? Reading mostly passed: agents got read-only access. Writing didn’t, so write access was limited to test environments, where the worst case was corrupted test data.
I also didn’t want the credentials inside the VM. With exe.dev integrations, the agent sends its request through a proxy, the proxy adds the credential, and the agent receives the response without ever seeing the secret.
Everything passed. I still didn’t want it.
At peak I was running about twenty VMs at a time. Not all of them were active. Some tasks got started, sat untouched for weeks, and were eventually abandoned because the cognitive overhead was too much and the work was never urgent or important enough to survive.
Validation sucked at that scale. I couldn’t manually rebuild every branch, rerun its tests, and check the application myself. But inside their isolated boxes, agents could run the tests, trigger the full CI suite, start the application, and drive it through a browser. They would send me screenshots of the completed work.
But the agent was still grading its own work. If it misunderstood what I wanted, it could build the wrong thing, write tests for the wrong thing, and then confidently tell me that everything passed.
That’s why being able to open the running environment mattered. I could use it myself, drive the new UI end to end, and make sure it worked the way I wanted, not just the way the agent said it worked.
Then there was the reviewing. When I wrote the code myself, I understood the change by the time it was ready for review. After all, I had made the decisions along the way. With an agent, the whole diff appeared at once. It might have passing tests and screenshots, but it was still unfamiliar code. And when several agents finished around the same time, I had a queue of complete changes to load into my head before I could merge any of them.

Having other agents review the code worked surprisingly well. They occasionally caught real bugs, and it was cheap enough to run several reviews. But I couldn’t merge something just because the agents approved it. I still had to understand the change. I was still responsible for the code that got merged.
And sometimes I understood it. The tests were green, the screenshots looked good, the UI did what I had asked for, even the schema looked good and the code was approved by all the review agents.

I threw it away anyway.
Maybe nobody needed it. Maybe it introduced a second way to do something we already supported. Maybe a small convenience added complexity we would carry for years.
The tools could tell me that the change worked. They couldn’t tell me whether it was worth adding to the system.
Unshipping something is so much harder than shipping it. Hyrum’s Law kicks in: once enough people use a system, someone depends on every observable behavior, even behavior you never intended to be a contract. Removing something breaks users, scripts, and workflows you didn’t know existed.
Shipping got easy. Deciding what to ship got important.
Not all agents write code
Everything so far has been about shipping code. But some of the most useful agents we run don’t ship code at all.
It’s worth pausing on what an agent actually is, because the word sounds heavier than it deserves. An agent is a model in a loop with tools. Send the model a message; if it asks for a tool call, run the tool and send the result back; repeat. That’s the whole thing.
The model is what makes the loop capable. Give it bash on a real computer, and it can install what’s missing, adapt when your grep has different flags, and keep going until the job is done.
The loop never changes. The tools decide what an agent can be. A development agent needs a full computer: shell, compilers, browsers, the freedom to install things. Hold those back and the agent is useless, and you’re back to approving every action.
My starting point: give the agent whatever I’d give a developer. Good developer tools have turned out to be good agent tools. Whether the best agent tools end up being developer tools at all, time will tell.
And the thing to fear isn’t any single tool. It’s combinations. Private data, untrusted content, external communication: any two are manageable. All three in one agent is how your secrets walk out the door. That’s what Simon Willison calls the lethal trifecta. So I don’t minimize tools blindly: I isolate the environment and I watch the combinations.
Here’s the first example: investigation. When a customer reports a problem, I have a prompt I use, and it’s embarrassingly simple:
“Customer reports: <their report, verbatim>. Please figure out what happened using the ClickHouse logs.”
Verbatim matters. If I summarize the report, the agent inherits my interpretation - and my blind spots. Given the customer’s own words, it queries the logs, reads the relevant code, and reconstructs what actually happened.
Then the decision is mine. Sometimes I ask for options and pick one. Sometimes the evidence says working-as-intended, and the fix is a doc or an email, not code. Either way I’m deciding from evidence, not guessing from a bug report.
The second job: attack. Our red-team agent has one instruction: try to break into our systems.
And it worked. It found open network paths we thought we had restricted, and showed us exactly how they were still reachable. We patched them before anyone outside ever noticed.
That’s more useful than a list of theoretical vulnerabilities. It took an assumption we were relying on and tested it against the running system, and the assumption was wrong.
The third job: watch. Deployments are scary, but not deploying is worse. We deploy in waves, and writing perfect rules for when to continue is basically impossible: production fails in weird ways.
So Athena babysits every deployment. It reads the diff, the metrics, and the logs, and watches the rollout. During one deployment it noticed a problem, investigated, and realized it was an infrastructure issue, not the new code. Instead of blindly halting everything, it continued deploying to the other machines.

Could I do a better job? I’m not sure anymore. Athena is more diligent than I can be. It doesn’t get distracted, doesn’t get impatient, and never stops paying attention. It is tireless.
Engineer the system before the agent writes the code
An agent can now design and build an entire system almost instantly. The design might even be good. But if I simply accept it, do I know how it works? Where it breaks? Which corners it cut? Which tradeoffs I agreed to?
At that point, I’ve inherited a legacy codebase that happens to be brand new. That’s vibe coding.
Agentic engineering is working with the agent on the system first: the architecture, interfaces, constraints, and tradeoffs. When the code arrives, I understand what I’m about to own.
There is no one answer to agentic engineering. Everyone works differently; every model is good at different things. The key here is reps: do a lot, ask a lot, throw away a lot. You’ll learn what works for you.
Everything we knew about running software teams is amplified now. Start with testing. When agents write and rewrite the implementation, unit tests that mirror the implementation just churn with it. Behavior, contract, and property tests matter so much more now: define the system’s invariants and pin down what must stay true while the code underneath changes freely.
So much of the game is migrations now. Code changes are cheap, but systems carry state: data, running processes, users mid-flight. Getting from one design to the next without dropping any of that is the part that’s still hard. The job is risk management and migrations.
The agent copies what it finds in the codebase. Good patterns amplify. Bad patterns amplify faster. Engineering hygiene is more important than ever: every pattern you tolerate becomes the template for the next hundred changes.
And it’s the golden age of personalized tools. You don’t have to be a domain expert to write a new linter. So write a new linter. Enforce the pattern, encode the lesson, make the mistake impossible. The marginal cost of a bespoke tool has collapsed.
Peer code review is dead. We don’t do code reviews at exe.dev: we merge code we deem should be merged. The review that matters happens earlier: in the design discussion, the contracts, the validation. By the time the diff exists, the decisions are already made.
Here’s a recent example. Shelley is the coding agent we ship in exe.dev VMs. I wanted its tools to be asynchronous.
The first design put the onus on the agent to decide which commands should run in the background. That introduced a ton of edge cases: large outputs, long-running tasks, blocked agents, and the agent having to predict all of this ahead of time.
If I were vibe coding, I would have accepted that design and let the agent deal with those edge cases. Maybe it would have handled them. Maybe it wouldn’t have. I wouldn’t really know.
Instead, we kept discussing the problem and found a design that removed most of the edge cases entirely. Every command follows the same path and automatically moves into the background after sixty seconds.
I didn’t remember this example off the top of my head. I asked an agent to search across all of my agent conversations for a case where my questions had materially changed the architecture. It returned the original request, the first design, the objections I raised, how the design changed, the final commit, and links to the original sessions.
This is why preserving every conversation mattered. The history wasn’t just an archive. I could query it to understand how I worked.
botd is dead; long live botd
There’s one more part of this story. botd died this month. It crumbled under its own weight.
That probably shouldn’t be surprising. botd was entirely vibe coded. I didn’t read a single line of its code, nor did I really pay attention to how it was architected. And at its core was a genuinely hard problem: driving every model family through its own native harness (Claude Code, Codex, and the rest) while papering over their differences. That is exactly the kind of problem where architecture matters, and I was not paying attention to it.
It helped me deliver an enormous amount of work, but it eventually became the exact thing I described earlier: a brand-new legacy codebase that I had inherited without understanding all of the decisions inside it. The architecture was exploratory, and some of the decisions buried in it made even modest changes mean redoing large parts of the system. And I wanted to build on top of Shelley so that I could control the harness better; botd was living on borrowed time. It just died before I could retire it.
But here’s the twist: that search I described earlier was never sent to botd. All the history was in SQLite, so I pointed another agent at it and got the analysis anyway. The tool died; the data didn’t.
When I started, my advantage was that the system lived in my head. Six months later, the honest answer is that I’ve lost some of that depth. I don’t have the same line-by-line familiarity with every system.
But I can ask agents detailed questions and use them to dig into whatever I need to understand. Work no longer waits for me to finish one thing before starting another.
I’ve shipped more in these six months than in any stretch of my career. I’ve also failed more: abandoned VMs, dead-end designs, a whole tool that collapsed. The failures were cheap, so I could afford a lot of them - that’s most of how I learned what works.
It’s the same loop that taught me to code, moved up a level: write, run, fail, fix. I used to iterate on code. Now I iterate on prompts, designs, whole features. A rewrite that used to cost a week costs a conversation. And understanding is iterative too: every conversation helps me understand and internalize the system more.
I stopped writing code by hand. I didn’t stop engineering.