Last week I opened a project I had built almost a year ago. Not because of a bug. A friend mentioned wanting a self-hosted Slack alternative, and I remembered I had made one. So I opened the folder in VS Code, looked at the file tree, picked the file that seemed like a sensible starting point, and opened it.
Five minutes later, I closed it.
I had written this code myself. And I could not tell what it did.
What I had built
Over the spring and summer of 2025, I built a Slack-style team communication tool. Channels, DMs, mentions, notifications, emoji reactions, file attachments, threads, unread badges. Most of what you touch in Slack on a normal day, I had implemented. It ran as a self-hosted theme on my own server.
The reason was ordinary. A paid Slack plan felt like too much for a small outside community, and I was riding the high of AI-assisted coding at the time. "I can probably build this myself." I started without thinking too hard about it.
In the first few weeks, channel lists and message posting came to life. I still remember how good that felt. Ask the AI, get code, paste it, watch it run, move to the next thing. The loop was fun, and somewhere in there adding features quietly became the goal in itself.
Then I shipped it to a client for review, and it went nowhere. No feedback came back, time passed, other work piled up, and the project drifted to the back of my mind. I did not touch it for almost a year.
What I saw when I opened it
Let me be specific about what I found, because the specifics are the whole point.
The first file I opened was the central controller. Dozens of backend files, more once you counted the JavaScript and CSS. I could not remember how I had arrived at this structure, or why.
The functions had names I could no longer read. Abstract labels like processMessage scattered everywhere, and I could not tell which one handled channel messages and which handled DMs without opening each one and reading the body.
So I grepped processMessage. It turned up in three files. The arguments looked similar, the bodies were subtly different. Past me had apparently meant "this one is for channels, this one for DMs, this one for threads." Reading it now, I could not see the difference. Three functions with the same name in three files, where the caller picking the wrong one breaks everything without a single error message.
The helpers had multiplied the same way. getUserNameById, getUserNameFromCache, getUserDisplayName, formatUserName. I wrote every one of them. Tracing which called which, and where each was actually used, ate far more time than I expected.
The comments came in two languages. A line like // Handle the case where user is not authenticated, and right under it a comment in Japanese about refreshing the unread badge. I had not cared at the time. The more honest version: I accepted whatever comments the AI produced and never rewrote them. Every time, I weighed "rewrite this comment" against "add one more feature," and the feature won.
Dead code sat everywhere. A function tagged // TODO: remove after refactoring, still there, called from nowhere. The refactoring never came. A few files were nearly empty, leftovers from a split I planned and abandoned, half-written with the rest still // TODO.
I checked the commit log to find out who wrote all of this. It was me. Every line.
Five minutes, and I closed the editor.
How I had been working
At the time, something like 80% of the code came from AI. I rotated between a few assistants, a CLI coding agent, an in-editor completion tool, and a chat model, building Slack features one at a time.
My day had a shape to it. Morning, coffee, open the editor, decide "today I implement unread counts for channels." Ask the AI how to do it Slack-style, skim the design it returned, say "go with this." The AI wrote the code. I copied and pasted. If it did not run, I pasted the error back, took the fix, and when it ran, moved on.
I had a loose sense of which tool to use for what. Structural thinking went to the CLI agent, small in-file edits to inline completion, research and rubber-ducking to the chat model. There was no real rule behind it. It came down to my mood that day, or whichever tab was already open.
The problem was that I ran at a speed my own understanding never caught up to.
It runs, next. It runs, next.
No spec. No design doc. The file layout was whatever occurred to me in the moment, function responsibilities stayed vague, and the feature count kept climbing. No tests at all. I sprinted in "just make something that works, fast" mode for about half a year.
My judgment got lazier as I went. "I think I wrote this already," I would think, and then searching the old files felt slower than asking the AI to write it again, so I asked again. That is how the same logic ended up in three places. That is where the duplicate-definition mess was born.
I still do not think using AI was the mistake. Without that speed, the tool would never have reached "done" at all. The mistake was quieter than that. While the AI wrote, I stopped acting as a reviewer and turned into a copy-paste middleman. I gave away the one job that was actually mine, deciding whether the code was any good.
The AI of early 2025 is not the AI of 2026
One more thing worth being honest about.
AI coding in 2025 had not matured into what it is now. The models I leaned on were strong for their moment, but a step below today's. The quality of the code, and the ability to hold a structure together across a project, were simply different.
Ask for the same thing in slightly different words and you got completely different code. It did not remember the previous design. Ask it to refactor one function and it would rewrite two neighbors along the way, and I would lose time diffing to work out what had even changed. Context windows were shorter, so pasting a long file left the AI quietly working from the first half only. I did not notice. I talked to it as though it had read the whole file, and many of its answers assumed just the top of it. Some of today's dead code almost certainly comes from places I waved through on "it runs, so fine."
Run a long project with an AI like that, and the code turns to spaghetti on schedule. I never adapted my style to the AI's quirks. Every new feature got a new session and a new file. "Start fresh" instead of "continue from last time," again and again, until the design had no through-line left.
Today's agents hold long context and reason across files. Ask for a feature and they read the project first, reuse an existing function when one fits, and follow the existing naming when one does not. The design consistency I fought for a year ago is now carried, in part, by the tool itself.
Code written with the AI of early 2025 still carries that era's limits, set in amber. I could hand it to a current model and say "clean this up," but the cleanup might quietly take the original behavior with it, so I have not pulled that trigger yet.
None of this is the AI's fault. Running without understanding its quirks was mine. I only wish the version of me who saw "AI coding is fast" had also seen the bill that comes due later.
How I am dealing with it now
Honestly, I am not, not yet.
There is too much source, and I am still working out where to even start reading. This is a long way from "jump in and fix it."
Here is the plan.
First, have a current AI read the whole codebase and give me a map, what each file does and where each function gets called, written back in something I can actually read. The starting prompt is roughly "list the five main files in this project and describe each one's responsibility in one short sentence." Having an AI explain code an AI wrote is an odd loop to stand in, but right now it is the most realistic move I have.
Next, hunt down and delete the dead code. That part is greppable, and static analysis catches it fast, PHPStan or Psalm on the PHP side, ESLint rules on the JavaScript side, flagging unused methods and unreachable blocks. I never ran any of it back then, so step one is simply getting the project to pass through the tools at all.
Then reorganize by feature. Right now channels, DMs, and threads share a file in some places and scatter across files in others. I want one feature per file or directory, channel/, dm/, thread/, notification/, attachment/, holding only what each one needs.
Only after that will I be ready to write a spec. Read the code, infer what past me was trying to do, and reconstruct the spec after the fact. This is the part I dread. "What did I build this for" is going to land on me more than once while I write it.
No deadline. I will chip at it between paying work. It might take half a year, or I might decide to rebuild from scratch. Rebuilding turning out faster is a common enough ending in real life.
Notes to my future self
I am writing this down now, because if I do not, I will do it all again.
Write a spec for each feature, however small, what this channel can do, when this notification fires, in plain language. When "I think I wrote this already" shows up, go find it instead of regenerating it. Keep an AI session continuous within a feature instead of opening a fresh one every time. And read what the AI writes as a reviewer, not a middleman.
That last one is really the whole thing.
The speed was real, and I would use AI the same way again. I would just stay in the chair as the person who decides what is good, instead of handing that chair to the model and reducing myself to the one who copies and pastes.
A reworked English version of a post I first wrote in Japanese.
Top comments (0)