Vibe coding multi-file state limits, traced to three specific failure points
The first vibe coding prompt produces a single-file toy and the model handles it fine. The fifth prompt has split things across App.tsx, four files in src/components/, two hooks, and a utility file. That is where the limits people complain about actually start. Most articles on this stop at “the model cannot see cross-file dependencies” and leave it there. Here are the three concrete points where multi-file state breaks, and the file paths in mk0r where each one is addressed (or honestly not).
Multi-file state in vibe coding breaks at three specific points: cross-turn drift when the builder re-rolls the project from a saved transcript and produces a slightly different tree each time, orphan components when a new file is created but never imported into the root component so the page stays blank, and partial undo when the tool restores some files but not others. mk0r addresses the first with a persistent ACP session that edits in place, the second with an explicit agent instruction to verify imports plus a Playwright check, and the third with git checkout <sha> -- . on the whole working tree. File paths throughout this page.
The three points where multi-file state breaks
Calling them “limits” in the plural matters. They are three separate mechanisms with three separate fixes. If you only name the wall as “the model cannot see cross-file dependencies,” you cannot route around it.
The cross-file failure modes, in the order you hit them
- 1
Cross-turn drift
The builder re-rolls the project from your saved transcript on every turn. Component names and folder structure shift between rolls.
- 2
Orphan components
The agent writes a new component file but forgets to import it into the root App.tsx. The page stays blank or unchanged.
- 3
Partial undo
Undo restores some files but not others. The tree ends up in a state that never existed in the project's history.
1. Cross-turn drift, fixed by editing the working tree in place
The first failure point appears the moment your project has more than one file. You prompt “add a streak counter,” the builder fires the prompt list at the model, the model produces a new project from scratch, and your perfectly fine NavBar.tsx is now slightly different in three ways you did not ask for. The model is non-deterministic and the input is always the prompt list, so the file tree mutates each turn.
mk0r runs each turn through one persistent ACP session that lives for the lifetime of the E2B sandbox. The session-reuse logic is in src/core/e2b.ts around line 909: a sandbox lookup keyed by the session key in localStorage, with the same sessionId carried across turns. The agent edits files on disk in /app rather than producing the project again. Every cross-file reference you established in turn one survives turn five, because the file-system at turn five is not regenerated from a transcript; it is the file-system at turn four with edits applied.
The same prompt sequence, two different multi-file behaviors
Each turn re-rolls the project from the prompt list. The model is non-deterministic so file structure shifts.
- NavBar.tsx changes shape between turns
- Components rename or merge unexpectedly
- Imports drift; sometimes a util is inlined, sometimes extracted
- By turn fifteen you cannot trust any file is what it was
2. The orphan-component bug, named honestly
Every multi-file vibe coder has hit this at least once. You prompt “add a star rating component.” The agent writes a clean src/components/StarRating.tsx. The preview refreshes. Nothing changes. The component file exists on disk, but nothing imports it into src/App.tsx, so React never renders it. The page is blank in the place where the rating was supposed to appear.
This is a real failure mode on every AI app builder we have tried, mk0r included. The mitigation lives in the agent's system prompt, not in code. The CLAUDE.md baked into the E2B template at /root/.claude/CLAUDE.md (you can read the source at docker/e2b/files/root/.claude/CLAUDE.md) tells the agent two specific things:
- “Always import and render new components in src/App.tsx. Components not imported from App.tsx will never appear on screen.”
- “After UI changes, navigate to
http://localhost:5173via Playwright MCP, take a snapshot, and check console messages. If the page is blank, verify the component is imported in App.tsx.”
The first instruction is a habit the agent is trained to apply before submitting a turn. The second is a recovery loop: after rendering, the agent checks its own output and self-corrects when the page is empty. Together they catch most orphans, but not all of them. When the agent does miss one, the fix is one prompt: “the StarRating is not showing up, check that App.tsx imports it.” The agent reads the file, sees the missing import, adds it, and the next turn renders correctly.
The honest framing is that this is a habit, not a guarantee. A vibe coder building seriously multi-file projects will hit one of these every couple of dozen turns. The right reflex when the preview goes blank is to suspect an orphan first, not a runtime error.
3. Partial undo, fixed by treating the whole tree as the unit
The third failure mode is the one that turns a salvageable mistake into a corrupt project. You undo a turn that touched seven files. The tool restores five of them and misses two, or restores all seven but produces a slightly different version because undo is implemented as a re-prompt. The result is a tree that never existed in the project's real history. From there, every new edit compounds the inconsistency.
mk0r treats the multi-file tree as the unit on both ends. Every successful turn writes a real git commit through commitTurn. The load-bearing line is git add -A: it stages every file modified, added, or deleted in /app at once. The resulting SHA is one snapshot of the whole project.
Undo is the symmetric operation: git checkout <sha> -- . where the trailing dot means “the entire working tree from this commit.” undoTurn at line 1731 reads historyStack[activeIndex - 1] and hands it to revertToSha at line 1691, which runs the checkout, stages, and commits the revert as an --allow-empty turn so the operation itself is part of the audit trail.
After undoTurn returns, ten files are byte-exact to the previous turn. App.tsx, every component, every hook, every util, the whole .env. The model is not consulted on undo, so the operation is deterministic, costs zero tokens, and cannot produce a tree that did not exist before.
And when you undo three turns and try a different idea, the slice at src/core/e2b.ts:1676 forks the history: historyStack = historyStack.slice(0, activeIndex + 1). Drop everything past the active pointer before pushing the new SHA. This is one line. Without it, undo plus a new prompt produces a tree that mixes the abandoned branch with the new one, and that is exactly when a multi-file project starts feeling unrecoverable.
What one turn looks like, end to end
The whole-tree-as-unit pattern is most legible in the sequence of one iteration turn. The user prompt enters one persistent session. The agent reads files from /app on demand, edits them in place, and the server commits the aggregate at the end.
One iteration turn that touches three files
Where the multi-file ceiling still bites
Whole-tree commits and persistent sessions push the wall back, but they do not erase it. The honest ceiling is the model's reasoning budget. When a project is small enough that the agent can fit the relevant files in context (or can read them on demand and reason about a few at a time), iteration holds up well. When the project grows past that, the agent starts forgetting cross- file conventions you established earlier: a Button styling pattern you set in turn three, a util signature you tightened in turn six, a hook contract you wrote in turn nine. The mitigation is the memory layer, mostly. The agent is told (in the same CLAUDE.md) to actively save user preferences and corrections as memories, and to read those memories at the start of each turn. That keeps intent stable past raw context length.
Past that, you stop vibe coding the project as one piece and start treating sub-folders as separate scopes: prompt only about the dashboard for a while, then only about the auth flow, then only about the admin panel. The agent does better when the unit it has to reason about is bounded. This is the same hack experienced engineers use when they delegate to a junior, and it works identically when the junior is an LLM.
The other ceiling is the envelope. mk0r generates working HTML, CSS, and JS or a Vite + React + Tailwind project. Multi-file iteration holds up well inside that envelope. It does not generate native iOS or Android, multi-user real-time state, or backend logic more complex than a single Postgres and a couple of Resend audiences can cover. If the requirement is in those zones, no amount of multi-file state hygiene fixes the mismatch; the right move is to bring in code and a developer.
Stuck on a multi-file vibe coding project?
Bring the prompt history, the file tree, and the place it broke. We will look at whether iteration can recover the project or whether the scope grew past what the envelope can carry.
Frequently asked questions
What does 'multi-file state' actually mean in vibe coding?
It is the part of your project state that lives across more than one source file. A single-file HTML toy has no multi-file state. The moment your AI builder starts splitting components into src/components/, hooks into src/hooks/, and utilities into src/lib/, every prompt now has to keep references consistent across files. A change to the props on Button.tsx has to flow through every place that renders Button. A new component file is invisible until something imports it. That set of cross-file references is the state. When people complain about vibe coding limits, this is often what they are bumping into.
Why does multi-file state drift between turns in most AI app builders?
Because most builders feed the saved transcript back to the model on every turn and ask it to produce the project from scratch. The model is non-deterministic. The first time it produced ten files; the third turn it produces eight or twelve. Component names shift. Folder structure changes. Files that used to import from one helper now duplicate the helper inline. From the user's perspective the project keeps mutating on edits you did not request. mk0r does not work that way. Each prompt goes to one persistent ACP session that edits the working directory in /app instead of producing the project again. The session-reuse logic is in src/core/e2b.ts around line 909.
What is the orphan-component bug?
It is the single most common multi-file failure mode in vibe coding. You ask the agent for a new component. It writes a clean Counter.tsx file. You refresh the preview. The page is blank, or unchanged. The agent created the file but never imported it into App.tsx, so React never renders it. The bug is real on every AI builder we have tested, including mk0r. mk0r's mitigation is explicit: the agent's CLAUDE.md (built into the E2B template at /root/.claude/CLAUDE.md) tells it that 'all new components must be imported in src/App.tsx,' and that after any UI change it must navigate to localhost:5173 in Playwright and verify the DOM rendered. When the page is blank, the instruction is to verify the import in App.tsx. The mitigation is a habit, not a feature, and the agent occasionally still forgets.
How does mk0r capture multi-file changes in a single turn?
Every successful turn calls commitTurn at src/core/e2b.ts:1635, which runs 'git add -A && git commit -m ...&& git rev-parse HEAD' inside the sandbox. The 'git add -A' is the load-bearing piece: it stages every modified, added, or deleted file in /app at once. A turn that touches App.tsx, adds two new components, deletes a third, and tweaks an entry in src/lib/ shows up as one SHA. The SHA is pushed onto session.historyStack and persisted to Firestore. There is no per-file commit, no concept of 'half a turn,' no possibility of a state where some files are versioned and others are not. The whole tree is the unit.
How does undo work when ten files are part of a single turn?
It runs 'git checkout sha -- .' inside the sandbox. The trailing dot means 'the whole working tree from this SHA.' undoTurn at src/core/e2b.ts:1731 reads the SHA at historyStack[activeIndex - 1] and hands it to revertToSha at src/core/e2b.ts:1691, which runs the checkout, stages, and writes a --allow-empty commit so the turn shows up in history. After it returns, all ten files are byte-exact to the previous turn. App.tsx, every component, every hook, every util. The model is not consulted on undo, so there is no chance of partial restore or non-deterministic re-roll.
What about when I undo three turns and then take a different direction?
mk0r forks the history on a one-line slice at src/core/e2b.ts:1676. Before the new commit lands, if session.activeIndex is anywhere except the tail, historyStack = historyStack.slice(0, activeIndex + 1) drops the abandoned future. This is how text editors handle undo. A lot of AI builders keep both branches alive with no clear current pointer, which is exactly when a multi-file project starts feeling unrecoverable around turn fifteen. The slice is one line of code; without it, undo plus a new prompt produces a tree that mixes the abandoned branch with the new one.
Where does multi-file state still hit a real ceiling on mk0r?
When the project gets big enough that the agent's context window can no longer hold the relevant files at once. mk0r runs Claude Haiku by default and the agent reads files from /app on demand rather than carrying the whole tree in the prompt, but a really sprawling project still fights the model's reasoning budget. The honest signal is when the agent starts forgetting cross-file conventions you established three turns ago: a Button component you styled a specific way, a util you renamed, a hook signature you tightened. The mitigation is the agent's memory layer (the same CLAUDE.md tells it to actively save preferences and corrections), but the ceiling is real. Past it, you stop vibe coding the project as one piece and start treating sub-folders as separate scopes.
Does the .env count as multi-file state?
Yes, and this is the part most builders fumble. The /app/.env in mk0r is written by buildEnvFileContent before the agent boots, with DATABASE_URL pointing at a real Neon Postgres 17 project, RESEND_API_KEY plus RESEND_AUDIENCE_ID, GITHUB_REPO_URL, and the PostHog keys for per-app event isolation. The provisioning is fired in parallel via Promise.allSettled inside provisionServices in src/core/service-provisioning.ts. Because .env is on disk inside the same git repo as the rest of the project (with the appropriate .gitignore for secrets), it is captured by the same per-turn snapshot mechanism. Restoring an old SHA restores the env shape consistent with that turn's code.
Can I see the file structure mk0r generates?
It is a Vite + React + TypeScript + Tailwind CSS v4 project under /app. src/main.tsx is the entry, src/App.tsx is the root component, src/components/*.tsx are the components, src/hooks/ holds hooks, src/lib/ or src/utils/ holds utilities, and src/index.css imports Tailwind. The Vite config and a small _mk0rBridge.ts hot-reload helper sit in src/ and are off-limits to edits. The agent's instructions (built into the E2B template at docker/e2b/files/app/CLAUDE.md and /root/.claude/CLAUDE.md) reflect this layout exactly. When something goes wrong in a multi-file way, those file paths are where you look.
Do I need an account to verify any of this?
No. Open mk0r.com, type a prompt, and you are an anonymous Firebase user with a real session key in localStorage and a private GitHub repo provisioned to that anonymous identity. Build a multi-file app, refresh the tab, undo a few turns, watch the whole tree rewind. The entire iteration loop runs without signup. Sign-in is enforced lazily through requireAuth on monetized actions like publishing to a custom domain.
Related
Other vibe coding limits, broken out
Vibe coding state, auth, and iteration limits
The three classic limits people bundle as 'vibe coding limits,' addressed inside one first-prompt flow with file paths.
The vibe coding iteration wall is four walls, not one
Regenerate-not-edit, no durable state, approximate undo, stale conversation buffer. Four mechanisms that produce the wall.
Best vibe coding tools
What 'best' means when you stop comparing on first-draft quality and start comparing on iteration discipline.