
Two AI agents, one HR system, and the one thing nobody told the next agent
Table of Contents9 sections
If you read my earlier post on git worktrees, this is the sequel nobody asked for: what actually happened when I stopped talking about worktrees in theory and ran two agents in anger across a real project. The worktrees did their job. The thing I didn't build is what bit me. Consider that post the setup and this one the consequences.
Anyone can vibecode now. That's not the interesting part anymore. You type, a model types back, something runs. Fine. The part that's still genuinely unsolved is whether a model can read someone else's messy, real codebase, actually understand what it does, and rebuild a coherent version from scratch. Not port it line by line. Re-derive it.
So that's what I actually tried. Not "can an AI write code," obviously it can. The question was sharper: can I point AI at an existing internal system, have it comprehend the business logic well enough to rebuild a renewed version from the ground up, build a real frontend on top, and end up with something I understand well enough to ship? And could I run two agents to do it, swapping between them based on which was stronger for the task in front of me, without them quietly sabotaging each other?
I liked the honesty of Bun's "Rewriting Bun in Rust" post, a team walking through a big bet including the parts that got ugly, not just the highlight reel. I wanted to write in that same spirit. Except my bet wasn't a language runtime. It was a workflow, and a question about how much a model can actually understand.
This is the honest write-up. It mostly worked. The way it didn't work is specific, and it's the actual reason I'm writing it down.
The real test: comprehension, not codegen
The project was a HR management system, employee records, leave requests, overtime, role administration, the usual. There was an existing legacy version. My goal was not to translate it. It was to see whether the AI could read that system, understand it, and cook a somewhat-renewed version from scratch, with a fresh frontend, while getting the business logic right.
That's a comprehension claim, and it's a harder one than "AI wrote some code." Writing code from a clear spec is solved. Reading an unfamiliar codebase, separating what it actually does from what it merely claims to do, and rebuilding it coherently is not. That was the thing I wanted to see.
Two agents, Claude and Codex, and here's the honest part: they both worked on both the frontend and the backend. I swapped between them based on which one was doing the task in front of me better, not by assigning fixed lanes. Sometimes Claude scaffolded backend and Codex polished frontend, sometimes the reverse. That flexibility was the whole point, and, spoiler, it's also exactly what set up the problem at the end.
Each agent lived in its own git worktree off the same base commit so their working trees couldn't overwrite each other. That boundary held. The one I forgot to build is the one that bit me.

The boring phase came first, and it was the whole experiment
Before either agent wrote a line of implementation code, I used Claude for the step I usually skip when I'm moving fast: read the entire legacy repo. Migrations, seeders, services, permissions, approval flows, constants, and the strange little calculations hiding inside the models. The instruction was explicit, do not implement anything yet.
What came out was a business-domain and gap document: what the old HR system actually supported, what a new one would need, and, the part that mattered most, which "features" were only mentioned in old docs but didn't exist in any executable code.
That last distinction is the entire comprehension claim in one artifact. The model wasn't just reading syntax. It was separating what the system says it does from what it actually does, and flagging the difference. That's the thing a junior engineer takes months to learn to do on an unfamiliar codebase, and it's the difference between rebuilding a system and rebuilding a rumor. It saved me from reconstructing ghosts, features that existed only as documentation folklore.
That phase also gave me somewhere to argue about architecture before an agent made it expensive to change. I asked about Next.js. The answer was, roughly: this is an authenticated internal app with a separate Laravel API, not a public SEO or content product, so React plus Vite was the simpler fit. I didn't take that as gospel, but it made the tradeoff concrete and left a written reason for not reaching for the fashionable default. A decision with a paper trail beats a vibe.
Only then did implementation start, tied to that document rather than to "make it better." This was never two models typing at once. It was read-only comprehension, then a written set of concerns, then implementation, then review. The parallelism gave me speed. The comprehension phase is what made the rebuild coherent instead of two confident guesses pointed at the same problem.
Building the frontend from an actual design, not a vibe
The frontend went up as a mock first, structure and flows wired to fake data, deliberately not pretty. Get the shape right before anyone argues about pixels.
Then came the part I'd skipped in every previous vibecode: instead of hand-waving "make it look good," I used an MCP connection to let the agent read a real design source directly and implement from it. That's the difference between "AI guesses at UI" and "AI reads the intended design and builds it." My OCD did the rest, because the first mock did not look good, at all. Inconsistent CRUD patterns, some full-page, some inline under a table, a cluttered page that badly wanted to be tabs, alignment bugs, a scatter of frontend decisions that just didn't make sense. Reading the design through MCP gave the enhancement pass an actual target instead of my vague taste as the only signal. The CRUD patterns got consolidated into a single reusable modal, the cluttered page became tabs, the alignment bugs got fixed against a real reference.
And because I was swapping agents across both surfaces, backend API work kept moving the whole time this frontend polish was happening. That's where the parallelism earned its keep, two surfaces progressing at once instead of waiting on each other.
The review pass caught things the author wouldn't
When a slice of backend work was "done," the instruction was: spawn a separate agent to review it before I look at it. Later I had one agent review the backend the other had written.
This paid off in a way I didn't appreciate until afterward. The review pass closed gaps that were never in the original ask. It wired up authenticated HTTP-level tests where only unit-level tests existed. It added a chronological sanity check on employment dates. It added a guard against demoting the last admin account, the kind of "what happens if there are zero admins left" edge that the person deep in writing the feature never stops to consider.
None of that was requested. It's exactly what a good reviewer catches and an author, buried in their own diff, misses. The split is the trick: the agent that wrote the code wants it to ship. The agent reviewing it, fresh eyes, no ego in the diff, wants to find what's wrong. Same reason humans don't let you approve your own PR.
Letting an agent touch real infrastructure, on purpose
At some point this stopped being a local toy and needed a real identity provider, a real tenant, real app settings, a real test login. Instead of doing all of that by hand and only ever letting the agent touch source code, I gave it direct CLI access to a dedicated, disposable auth tenant, with one written rule: this tenant is throwaway, create test users, change config, generate and burn tokens freely, no need to ask each time, and that permission does not extend to anything that isn't clearly this same disposable tier.
That paid off directly. When deploying surfaced a real config gap, the deployed frontend didn't have the values it needed to even render a login screen, the agent used that same access to fix the actual provider-side settings and provision a working test account, instead of me relaying config values back and forth by hand across a chat window.
Same principle as the worktree boundaries, just pointed at external infra instead of source control: give real access, but keep the blast radius to something you can afford to blow up.

And yes, I deployed it and dropped the link in the team group chat. Someone who wasn't me logged in, on real infrastructure, and did not immediately hit a 500. That was the bar. It cleared it.
Where it actually went sideways
The failure mode wasn't "the agents wrote bad code." It was: nothing told the next person, human or agent, that any of this had happened.
I picked the project back up in a fresh session days later, with no memory of having set up two worktrees. I found the second one by accident, running a plain ls on the parent directory. From there I had to reverse-engineer what it was, whether it was safe to touch, what state it was in, reading branch names, comparing file timestamps, diffing a stale draft doc against a newer one. That is not free. Guess wrong and you delete something's uncommitted work.
Two concrete bugs came straight out of that gap, and both trace back to the same root: I swapped agents across both surfaces freely, so no agent owned anything, and no shared record tracked what any of them had touched.
A silently overriding stylesheet. One agent had added an entire extra CSS file, loaded after the main one, quietly overriding rules at equal-or-higher specificity. Nothing recorded its existence anywhere, no comment, no commit message I'd seen, nothing. I burned a real debugging session on "why is this column right-aligned when nothing in the code says it should be" before I even knew a third stylesheet existed to go looking for.

An orphaned test row that corrupted a real number. Somewhere in an earlier pass, a leave-history row got inserted directly against the database, not through the normal request-and-approval flow, for testing. It had no audit trail. Its leave type didn't match its own stated reason. It had no link back to any real request. It just sat there looking plausible.
Hours later, though it felt like months, the recalculation logic that keeps balances in sync summed that phantom row in as if it were real. It quietly wiped out an employee's actual imported leave history and replaced it with a bogus 0.5 days remaining. And because the row looked almost legitimate, nothing flagged it. Tracing it back meant walking the recalc logic by hand, finding the input that made no sense, then realizing the input itself was the bug, a row no request had ever created, doing damage no code review would have caught, because it never showed up in a diff. It lived in shared state, not in anyone's branch.

Neither of these is "the AI messed up" in the bad-code sense. Both are: two independent lines of work touched the same shared state, there was no shared log of who did what, so the problems surfaced later as unexplained mysteries instead of immediately as obvious diffs.
Why Bun got away with 64 agents and I got bitten by 2
Here's the thing that reframed the whole experiment for me.
Bun's Rust rewrite ran sixty-four agents in parallel for eleven days and shipped. I ran two and got knifed by shared-state drift. The difference isn't agent count. It's that Bun had a shared, non-human source of truth every agent bottomed out against: a language-independent test suite with over a million assertions, a strict "commit specific files, never stash or reset" rule, and written porting and lifetime guides every agent read first. When a Bun agent did something wrong, the shared oracle caught it immediately, in CI, as a red diff. Not days later as archaeology.
My two agents had no shared ground truth. No append-only log of side effects, no shared record of "this worktree exists and here's its scope," nothing watching the database both agents could reach. So drift didn't surface as a failed assertion. It surfaced as me running ls and going "wait, what's this folder."
That's the actual precondition for parallel agents working at all. It was never the count. It's whether there's a shared source of truth that isn't a human's memory. Bun had one. I didn't. Everything that went wrong lives in that gap.
The actual lesson
If I did this again, I'd add exactly one cheap thing: a shared, append-only note. As dumb as a WORKTREES.md with one line per active worktree, "worktree B, started <date>, scope: leave policy engine plus audit trail," that any agent or human picking up the project reads first. Not a framework, not a tool. Just a place where "this parallel thing exists and here's what it's for" gets written down before it's forgotten.
The other half: commit more often, even to a branch nobody's reviewing yet. The dangerous state was never "work happened in parallel." It was "work happened in parallel and stayed uncommitted long enough that nobody, including future-me, could reconstruct why it existed."
And I'd make the comprehension document a real artifact, not just chat context. That domain map was the best part of this whole thing, the actual evidence the AI understood the system, and it almost evaporated into a scroll-back nobody would read again. Put it in the repo. Link the active worktree to it. Make "read this before you change a business rule" an actual handoff rule, not a hope.
Would I do it again
Yes. The comprehension held up, the AI read a real legacy system, told me what actually existed versus what was folklore, and rebuilt a coherent renewed version from scratch with a real frontend on top. The parallel throughput was real. The review pass caught things a single pass wouldn't have. I'd just stop relying on archaeology to reconstruct state after the fact, and spend the ten cheap minutes to give the agents a shared trail instead.
Anyone can vibecode one thing with one agent now. Getting a model to actually understand an unfamiliar system and rebuild it, while running several agents at once and keeping the plot, is the part that's still hard. And it turns out the fix isn't smarter agents. It's a shared piece of paper they all have to sign.