From Prototype to Real Software
When to stop vibe coding, and the path from a Pen to software others can rely on: exporting the code, version control, tests for the logic, code review, continuous integration and agents working under supervision.
Prototype or product
Over eight steps you built a game by describing it, and the theory pages in between asked you to do a little more each time: read the code, save versions, debug with the error message, write your own prompts. That is the arc this page completes. Simon Willison drew the line in March 2025: "If an LLM wrote the code for you, and you then reviewed it, tested it thoroughly and made sure you could explain how it works to someone else that's not vibe coding, it's software development" (Not all AI-assisted programming is vibe coding).
A year after he coined the term, Andrej Karpathy described where professional practice had gone: programming through AI agents was "increasingly becoming a default workflow for professionals, except with more oversight and scrutiny", and his name for it was agentic engineering (4 February 2026; see What Is Vibe Coding?). The AI still writes much of the code. What changes is who answers for it.
Code ownership is being responsible for a piece of code after it is merged or shipped: understanding it, maintaining it, fixing it when it breaks and answering questions about it. When you accept generated code into a project others rely on, you take on that ownership, whoever or whatever typed it. That is the practical meaning of human accountability.
When to stop vibe coding
Vibe coding exit criteria are the conditions under which a project should move from accepting code unread to reviewing, testing and versioning it properly. Willison's own conditions for vibe coding, "low stakes" and "consider security", turn into a practical list. Stop when:
- Other people will use it, or depend on it being there tomorrow.
- It stores data, handles logins or touches money. Personal data brings legal duties, and a login is an attack surface (see Risks of Vibe Coding).
- It costs money to run: a paid API, a server, a cloud account that bills by use.
- You cannot explain a part of it to someone else.
- The fix loop has started: each fix breaks something else, and you keep pasting errors back without understanding them (see Debugging with AI).
The space game meets none of these, which is why vibe coding it was fine. Add online high-score lists with player names, and it meets the first two at once.
Keep it or build it again
Prototype debt is the technical debt a prototype carries into a product: shortcuts that were fine for a demo, such as missing error handling, no tests, hard-coded values or code nobody has read, which become costly once real users depend on it. Every prototype has some; the question is what to do with it.
A rewrite decision is the explicit choice, made when a prototype is to become real software, between improving the existing code step by step and building it again with the prototype as a specification. Keeping it makes sense when the code is small, you understand it and tests can be added around it; that is refactoring. Rebuilding makes sense when nobody understands the code, when it was built on assumptions that no longer hold, or when its structure fights every change. Either way, it is a decision someone makes on purpose, not something that happens because the demo was quietly put into production.
Back to the Kestrel Energy example from What Is Vibe Coding?: Ravi Kumar's Friday-afternoon "my usage this week" chart did its job when Isabel Santos saw it and said the idea was worth building. The version for customers is built again by the team: in the portal's repository, on a branch, with tests for the numbers it shows, and reviewed before it is merged. The vibe-coded demo answered the question "should we build this?"; it was never the answer to "how?".
The path out of the Pen
For the game, and for most small projects that grew, the steps are the same. Each one is a topic of its own in this encyclopedia; this page only shows how they connect.
Export and version control
CodePen's export gives you a .zip file with a
src folder (your files), a dist folder
(the last build) and a LICENSE file; on the free plan
any logged-in visitor can export a Pen (as of October 2026).
Unpacked on your own computer, those files become a Git repository
with a few commands. The example below uses three tiny files in
place of the real ones; the commands are the same:
# The unpacked export, shortened to three tiny files.
mkdir -p retro-jet/src
cd retro-jet
printf '<canvas id="game"></canvas>\n' > src/index.html
printf 'body { margin: 0; }\n' > src/style.css
printf 'import * as THREE from "three";\n' > src/script.js
printf 'dist/\n' > .gitignore # the build output is made again, not saved
git init -q
git add .
git commit -q -m "Import the game from the CodePen export (step 8)"
git log --oneline
git ls-files
f30a337 Import the game from the CodePen export (step 8) .gitignore src/index.html src/script.js src/style.css
From here on, every change is a commit you can look at and undo. What the commands do, and how to work with branches and remotes, is in the Git topic: What Is Version Control?, Your First Repository and Staging and Committing. The forks and exports you made on Saving Versions and Going Back were version control by hand; this is the real thing.
To run the game outside CodePen you also need what CodePen did for
you: resolve import * as THREE from "three" and serve
the files. The three.js manual calls npm plus the build tool Vite
"the recommended approach for most users". In outline, with the
version pinned:
npm install three@0.186.1
npm install --save-dev vite
npx vite src
npm records the exact versions in package.json and a
lock file, so the game no longer picks up whatever Three.js version is the
latest (see
Modules and npm).
Your files may need small changes for this setup, such as the script
tag in index.html; the manual's installation page shows
the layout it expects.
Tests for the logic
The game's rules were written as pure functions at the top of
script.js: numbers in, numbers out, no Three.js and no
page. That choice pays off now, because pure functions are the
easiest code there is to test. A
unit test
calls a function with known input and checks the result. The
functions below are copied unchanged from the step 8
script.js; the tests after them are new:
const assert = require("node:assert/strict");
// Copied from the GAME RULES section of the step 8 script.js.
const COMBO_WINDOW = 1.5; // seconds: hit again within this for a combo
const MAX_COMBO = 5;
const HIGHSCORE_KEY = "retro-jet-highscore"; // the name it is saved under
// Quick kills build a combo (x2, x3 ... up to x5); a slow one resets it.
function nextCombo(combo, sinceLastKill) {
return sinceLastKill <= COMBO_WINDOW ? Math.min(MAX_COMBO, combo + 1) : 1;
}
// The highscore saved in the browser, or 0. Reading storage can throw
// (in a sandboxed frame it does), and the saved text can be rubbish.
function loadHighscore(getStorage) {
try {
const saved = Number(getStorage().getItem(HIGHSCORE_KEY));
return Number.isFinite(saved) && saved > 0 ? Math.floor(saved) : 0;
} catch {
return 0; // no storage: the highscore lasts until the page closes
}
}
// P switches between "playing" and "paused"; other states stay.
function togglePause(state) {
if (state === "playing") return "paused";
if (state === "paused") return "playing";
return state;
}
// The tests: fake storage instead of the browser's localStorage.
const saved = (text) => () => ({ getItem: () => text });
const blocked = () => {
throw new Error("SecurityError");
};
const tests = [
["a quick kill raises the combo", () => assert.equal(nextCombo(2, 1.0), 3)],
["the combo stops at x5", () => assert.equal(nextCombo(5, 0.2), 5)],
["a slow kill resets the combo", () => assert.equal(nextCombo(4, 2.0), 1)],
["a saved high score is read", () => assert.equal(loadHighscore(saved("1250")), 1250)],
["nothing saved counts as 0", () => assert.equal(loadHighscore(saved(null)), 0)],
["rubbish counts as 0", () => assert.equal(loadHighscore(saved("lots")), 0)],
["blocked storage counts as 0", () => assert.equal(loadHighscore(blocked), 0)],
["P on the game over screen does nothing", () => assert.equal(togglePause("over"), "over")],
];
let failed = 0;
for (const [name, test] of tests) {
try {
test();
console.log(`ok ${name}`);
} catch {
failed += 1;
console.log(`FAIL ${name}`);
}
}
console.log(`${tests.length - failed} of ${tests.length} passed`);
ok a quick kill raises the combo ok the combo stops at x5 ok a slow kill resets the combo ok a saved high score is read ok nothing saved counts as 0 ok rubbish counts as 0 ok blocked storage counts as 0 ok P on the game over screen does nothing 8 of 8 passed
In a real project you would use a test runner (Node.js has one built
in, node --test) and keep the tests in their own file,
but the idea is this. Once the tests exist, they are also the best
thing to put in a prompt: ask for a change and say that these tests
must still pass, or write a failing test first and ask for code that
passes it (see
test-first prompting
and
Test-Driven Development). The tests, not the model's confidence, decide whether the code
is right. Which kinds of tests a larger project needs is on
Test Levels and Test Types; the look and feel of a game still needs a person to play it.
Review and continuous integration
A review gate is a rule that no change, whether written by a person or an AI, reaches the main branch until it has been reviewed and has passed the agreed automated checks. In practice it is a pull request that needs an approving review and green status checks before it can be merged. It is the point where someone who did not write the code, and who must be able to understand it, says yes.
AI can help on both sides of the gate: an AI code review is a useful first reader, but it does not replace the human one. Behind the review sits a continuous integration pipeline that runs the tests, a linter and security checks on every push, with quality gates that block a merge when they fail, and repository security such as secret scanning that catches a pasted key. None of these care who wrote the code. That is exactly what makes them work for generated code.
Production hardening
Production hardening is the work of making software that runs in a demo fit for real users: handling errors and unexpected input, securing logins and data, removing secrets from the code, making it accessible, measuring whether it works, and planning how to undo a bad release. A prototype skips all of this on purpose. Software that people depend on cannot.
- Security: input validation, authentication, access control, secrets in a secrets store, dependencies checked (see Web Application Security).
-
Errors: what happens when storage is blocked, the
network is down or the input is rubbish. The game's
loadHighscore()above is a small example. - Accessibility: keyboard use, contrast, reduced motion (see Accessibility Basics).
- Operations: logging, monitoring, backups and a way back. Larger teams check this formally in a production readiness review.
This list is also where a minimum viable product differs from a prototype: the product may be small, but what it does, it does properly.
Agents under supervision
A supervised agent is a coding agent that works inside limits a person has set and checks: it runs in a repository or a sandbox with only the permissions the task needs, follows written project instructions, has its changes tested and reviewed before they are merged, and needs approval for anything risky or hard to undo. This is what Karpathy's agentic engineering looks like day to day, and it is where this topic ends.
-
Instructions in the repository. Coding agents
read standing instructions from files such as
AGENTS.mdorCLAUDE.md: how to run the tests, which conventions to follow, what never to touch (see instructions files). - Least privilege. An agent works on a branch and a development copy of the data, never with production credentials. The Replit incident on Risks of Vibe Coding happened because an agent "had direct access to production databases".
- Small, reviewable changes. One task per run, as in Prompting in Small Steps; a diff of 2,000 lines is not reviewed, only approved.
- Tests as the judge, and a person who reads the diff, merges it and owns the result.
The habits are the ones this topic has built step by step: describe one change, read what you got, test it, save a version you can go back to. With a Pen they protect an afternoon's game; with Git, tests, review and CI they protect software other people rely on. How agents work under the hood is on AI agents and tool use, and the working habits around production on Working Safely on Production Systems.