The Claude Code Playbook for Finishing What You Start
How one person shipped a live multiplayer 3D browser game — and the fourteen skills that made it repeatable.
How one person shipped a live multiplayer 3D browser game — and the fourteen skills that made it repeatable.
by Broadened Horizon
You have built something impressive with an AI coding agent. Probably several things. A dashboard that queried real data. A game prototype that ran. A tool that did the thing you wanted, on your machine, in a browser tab you left open so you could show someone later.
None of them survived contact with a real user.
Not because they were bad. Because "runs on my machine while I watch it" and "runs for a stranger who does something I did not anticipate" are two different products, and the agent got you all the way to the first one in an afternoon.
That is who this is for. Not beginners. People who are already fast and still have nothing live.
If you have never opened a coding agent, this book will still make sense, but it will read as a warning rather than a diagnosis. Come back after your third stalled demo.
This is not a prompt pack. There is not a single "act as a senior engineer" line in it.
This is not a tutorial. I am not going to teach you a framework, and the code in here is mostly commands you can run against your own project, not code you copy into it.
This is not theory about where AI is going.
It is ten failures. Each one cost real money, real days, or a broken production build with people logged in at the time. Two of them were outages on a live game, and both were invisible to a test suite that was reporting green while they happened. One was a pilot that passed on two files that both stored their positions as plain floats — so the pilot could not have caught the bug that then broke every quantized asset in the batch. One was a disk with 0.13 GB free on a 1 TB drive.
I did not read about these. I paid for them.
Every chapter is one rule. Each chapter opens with what broke and what it cost, names the instinct that caused it — the instinct is almost always the reasonable one — states the rule in a single line, and then gives you something to run today.
The chapters are ordered by when the failures bite you, not by how important they are. Chapter 2 fires before you write a line. Chapter 5 waits until you have users. If you read straight through, you are walking the timeline of a project that shipped.
Read it in order the first time. After that, it is a reference. Find the chapter whose opening failure sounds like your week.
01
The game was finished on a Tuesday.
Characters loaded. The world streamed in. Movement felt right, the camera behaved, combat resolved, the database persisted state across a restart. I had a stylized 3D multiplayer world running in a browser on a zero-dependency stack — Node, SQLite, and a WebSocket server I wrote by hand. I opened two windows side by side and watched two characters walk around the same world. It worked.
Then I gave it to people.
What followed took the project past 300 shipped builds in total. Not features — most of those builds changed nothing a player could name. They were the difference between a thing that works when I use it and a thing that works when someone else does.
Somewhere in there: about 450 AI image generations, because the concept art that looks good in a grid of four looks wrong the moment it is a texture on a mesh in motion. About 190 3D conversions, because an image that converts cleanly still arrives with a skeleton that does not match your rig. An asset pass that took the world from 9.9 million triangles down to 1.66 million across 131 files, because the models that made the game look good were the reason it took forever to load.
And two production outages. Players hitting a black screen. In both cases the test suite was green, because the test suite was reading the file, and the failure was in the browser's boot order — a layer no grep could see.
The build took days. The ship took months.
The instinct is to read that gap as a capability problem. The last 20% is the hard 20%, the reasoning goes — harder problems, so of course they take longer. Get a better model, a better agent, better prompts, and the tail compresses.
It does not. I have watched the model get better across this build, and the tail did not move much, because the tail is not made of hard problems. It is made of a different kind of problem.
Building is a closed world. You define the inputs, you run the thing, you look at it, you fix what you see. The feedback loop is you.
Shipping is an open world. The inputs are strangers. The failure does not announce itself in your terminal — it shows up as a player who logs in, sees nothing, and never comes back, and you find out three days later by accident. Every bug in the second half of this book was silent. Not one of them threw an error I saw.
Agents are extraordinary in a closed world. They are as blind as you are in an open one, and they are much faster, so they build the wrong confidence quicker.
The last 20% is not harder work. It is a different failure surface — silent instead of loud — and almost nobody writes that one down.
Before your next project goes to a real user, answer these five in writing. Not in your head. Writing, because the vague ones stay vague until you try to type them.
If any answer is "I would have to check," that is the chapter you need. The rest of this book is those answers, in the order they came due.
One note on the toolkit. Every rule in this book was eventually turned into something automatic, because a rule you have to remember is a rule you will skip at 2 a.m. Those became 14 skills for Claude Code, and I will name the relevant one at the end of each chapter. This chapter's rule does not have a skill. It has the other nine chapters.
I can make all of this abstract and it would still sound reasonable. So here is the only part that is not an argument.
The game is live. It is at https://eternalexpanse.com. You can open it right now, make a character, and walk around. You will not be the only person who has one there.
That is the whole claim. Not a repo, not a demo video, not a screenshot of something that ran once on my machine. A URL that a stranger can type, that works, that has real players in it.
Everything in the next nine chapters is what it cost to be able to say that sentence.
02
I once spent a good chunk of a session writing a CDN edge-warmer.
It was decent work. It walked the public asset tree, requested every file a player downloads on first load, and it handled a caching trap I was quietly pleased to have caught. I wrote the explanation into the file header so the next session would understand why the odd-looking part was there.
The repo already had one.
warm_edge.cjs had been sitting in scripts/ the entire time. It handled the trap I had caught, plus a second trap I had missed completely. It asserted on the right signal instead of the one that merely looks right. It exited non-zero when the warm was a false green — a different failure from this one, with its own section in Chapter 3, and the reason that tool exists at all. And it took the host as an argument, so it was portable enough to ship inside a plugin. Mine was a worse duplicate with my own machine's hostname baked into it.
The command that would have prevented all of it is ls scripts/. One call.
Here is the part that stung more than the wasted time. That lesson was not new. It had been learned before, and written down, in a file, in that project, on the disk in front of me. The knowledge existed. The session just never looked.
Almost everybody treats this as a storage problem. The reaction to "the agent forgot" is to write more down — a longer CLAUDE.md, a fatter handoff, a memory file with everything in it. So the documentation grows, and the forgetting continues, and now there is more unread text than before.
It is a retrieval failure. The notes were fine. The reading never happened.
There is a second instinct, subtler and more expensive: reading the handoff and believing it. A handoff document is a photograph of a moment. It was true when it was written. Since then another agent shipped three times, a file was renamed, a flag was removed. If you start searching from a claim in a handoff, and the claim has rotted, you will search a codebase that no longer exists and conclude the feature is missing. Then you will build it again, next to the one that is already there.
Read what already exists before you write anything — and treat every claim in a handoff as a hypothesis until one grep confirms it.
Five steps. It costs about two minutes and it has repeatedly turned three rounds into zero.
1. Read the locks. If the project has a CLAUDE.md or an AGENTS.md, read the headings at minimum. Every heading in a good one is a scar — a bug that already cost somebody a day. A lock you did not read is a bug you are about to repeat.
2. List the tooling before you write any. One call: ls tools/ tools/probes/ scripts/. Duplicate tooling is how a repo rots. Two warmers, one of them quietly warming URLs nobody requests, and no way for the next session to tell which is canonical.
3. Capture the baseline BEFORE you change anything. Not as a number you remember — as a file you can diff. Run the check suite and record the output. "86 pass / 1 pre-existing stale / 0 new" is the only thing that lets you tell a failure you caused from one that was already there. Without it, the first red line derails the session and you start bisecting your own work for a break that predates you.
The important detail: record a named roster, not a total. Two failures only a roster catches:
A flagged delta is not automatically a bug. It is a claim you now have to make out loud: did you mean that? The tool cannot know your intent. It can only refuse to let the change pass in silence. Intended, re-capture. Not intended, you just caught it before it shipped.
4. Read the handoff as hypotheses. If there is a HANDOFF_MASTER.md, an authority map, an asset manifest — read it, then spend the one grep. Confirm the file, function or flag it names still exists before you search from it.
5. Check the memory index. Same rule. Notes reflect what was true when written. If one names a path, verify the path.
Paste this into your project and make it the first thing that happens:
PREFLIGHT — before the first edit
[ ] Read CLAUDE.md / AGENTS.md headings
[ ] ls tools/ tools/probes/ scripts/ # before writing any new tool
[ ] Run the check suite; save the NAMED roster to a file
[ ] Read the handoff — then grep to confirm one claim before using it
[ ] Skim the memory/index — verify any file or flag it names still exists
One more thing, because it is the step people skip. Prove the gate fires. A check that has only ever been seen to pass is not known to work. Inject the exact damage it claims to catch, assert it exits non-zero and names the thing, and restore the file byte-for-byte in a finally. The first time I did that on a corruption guard, the guard failed its own test — it caught one flavour of mojibake and missed another entirely. It had been green for weeks, guarding nothing.
In the bundle this is the session-continuity skill, and the preflight above is its front half — the part that runs before you touch anything. What it does with a lesson after the round, so the next session actually finds it, is Chapter 8.
03
A whole pass of fixes. Written, reviewed, version-bumped, deployed, reported live.
The served file never changed.
I spent hours after that debugging behaviour in code that was not running. Every hypothesis was reasonable. Every test was inconclusive, because the thing under test was a file the server had never read. The change was real. It was committed. It was invisible.
This is the most common way a change "doesn't work" on a live system, and it has almost nothing to do with the change.
On the game — https://eternalexpanse.com, real players, no staging gate between me and them — the layout looks like this:
game.js <- you edit this
public/game.js <- the server serves this
index.html <- and this
public/index.html <- and this
Two files. Same name. One of them is the deploy target and one of them is a draft. Every editor, every diff tool, every commit will happily tell you the change landed, because it did — in the copy nobody serves.
The wrong instinct is treating the absence of an error as evidence of success. No exception was thrown. The copy step printed nothing red. The deploy script said done. So it worked.
It did not work. Nothing checked.
A success message is not a verification. A handler that saves and then reports success will report success even when the thing it saved was normalised away to null on the way in. The acknowledgement is generated by the same code path you are trying to test. Read the state that persists, not the message about it.
The file you edit and the file the server serves are two different files. Prove the copy by hash, never by the absence of an error.
node -e "const fs=require('fs'),c=require('crypto');\
const h=p=>c.createHash('sha256').update(fs.readFileSync(p)).digest('hex').slice(0,16);\
console.log(h('game.js'), h('public/game.js'))"
Two hashes. They match or the deploy is not done. That is the whole gate, and it takes a second.
Around that, five things that each cost me a round to learn:
Read the current version before you bump it. Do not assume the number. On a project where a second agent also edits the files, the version may already have moved — this happened to me twice in one session. Assume wrong and your find-and-replace matches nothing, updates zero files, exits clean, and ships a stale cache key while reporting success. Every player keeps the old client.
Bump the cache key in both copies of index.html. The game.js?v=NNN query is what breaks the browser cache. If the served copy still points at the old key, the new file exists on disk and no browser will ask for it. And never change content under an already-published key. Cut a new version instead.
Do not bump the client for a server-only change. The reverse mistake. Bumping the client version and cache key for a change that only touched server code forces every player to re-download the entire client and buys nothing.
Watch the line endings. The two files may not agree — one CRLF, one LF. Any patch helper that searches for an anchor string must try both forms, or it reports NOT FOUND on a string that is plainly there in front of you.
Verify over HTTP, not on disk. Grep the response the server actually returns. Disk state is where the bug lives.
After the twins are in sync, the edge still has to be warmed, and this is where a clean run means the least.
A freshly written warmer once fetched every asset, reported a confident 276/276, ERR=0, 550.8 MB, and cached nothing at all. Half a gigabyte of origin traffic for zero benefit, and every player still cold.
Two independent reasons, both invisible in the status code:
.glb. Same asset, two URLs, measured live: /assets/species/ashen_suit.glb came back HTTP 200 with cf-cache-status=DYNAMIC — not cached. /assets/species/ashen_suit.glb.js came back HTTP 200 with cf-cache-status=HIT. Identical status code. Opposite outcome./foo.glb.js when the client requests /foo.glb.js?<token> and you have warmed a URL nobody will ever ask for.The corrected run reported HIT=126 MISS=146, and a second pass HIT=170. Those are numbers that can move — the first pass left the edge in a different state than it found it, and the second pass is what proves it did. 276/276, ERR=0 cannot move. A warmer that caches nothing prints exactly the same line as a warmer that caches everything, which is why it reads as success, and keeps reading as success, on every run forever. That is the whole anatomy of a false green: a measurement taken on a channel that is not connected to the thing you care about.
Assert on the cache-status header, not the status code. And mirror the client's own URL builder rather than re-inventing it — read the shipped code and construct the request the way the game does, or you are warming a parallel universe.
The deploy sequence, in order, none of it skippable:
1 Back up, to a timestamped folder outside the project tree
2 Syntax-check server AND client — through the consumer's parser
3 READ the current VERSION / CLIENT_VER
4 Bump the server version
5 Bump the cache key in BOTH index.html copies
6 Copy the twins — verify by hash
7 Restart with the data dir pinned explicitly
8 Verify markers over HTTP against the served response
9 Warm the edge — assert on cache-status, expect ERR=0 AND hits
In the bundle, live-game-deploy carries this sequence, and two of the scripts do the parts you should not be doing by hand: deploy_twins.cjs copies and hash-verifies the pair, and fails the deploy on a corrupted file; warm_edge.cjs walks the public tree, requests each asset the way the client does, and exits non-zero on a false green.
04
A hairstyle sat too low on a character's head. Cosmetic. Obvious. A five-minute fix.
I raised it twenty percent. The report came back: it hasn't moved. I raised it thirty-five. Hasn't moved. Forty. Forty-five. Fifty. Fifty-five. Six rounds, six bigger numbers, and every round the same sentence came back at me — I keep saying this, but I never see that hairstyle move.
He was right every single time.
The constant I was tuning was applied to the wig before the piece was scaled down to fit the head, and that piece's scale was a small fraction. So the full range of that constant — zero to maximum, the entire lever, every value it could ever hold — moved the hair 0.45 mm. I had spent that range by round three. Everything after was arithmetic on a number that had already run out.
The real lever sat further down the chain. Applied at full scale. Headroom measured in centimetres, not fractions of a millimetre. One line. It had been sitting there the entire time.
What it cost wasn't the hair. It was six rounds of a two-person loop — one person ships, one person playtests a live game with real players on it — burned proving that a number I already controlled did nothing.
The instinct is that the number wasn't big enough.
It is a reasonable instinct. Trying another value always costs thirty seconds, which is exactly why you can do it eleven times before you notice the afternoon is gone.
The second instinct is quieter and worse: when the result contradicts the person looking at the screen, you start discounting the person. Maybe it moved a little. Maybe he's on a stale version. No. When someone tells you nothing moved, nothing moved. That is a measurement, and it is a better one than the one you are not taking.
On the second failed fix of the same fit or tuning problem, stop changing the value and ship instrumentation instead.
Not the third. Not "one more try, and then I'll instrument." The second. One failure can be a typo or a cache. Two failures in a row on the same knob means your model of the system is wrong, and no value you pick out of a wrong model is going to land.
A constant's name tells you nothing about its authority. capAnchor.y sounds like it controls where the cap sits. It does — right up until something downstream multiplies it by a fraction and hands the result to the renderer.
So before you touch a number, answer one question: how far does this constant's entire range move the thing I can see? Trace it forward, through every multiply, every scale, every space conversion, to the value that actually reaches the output. Then report the change in real units — millimetres, pixels, milliseconds — never in percent.
Percent is the trap. Percent is a number about a number. Millimetres are a number about the world.
The log line you want the running code to print looks like this:
[fit] piece=<name> lever=<constant path>
before=<v0> after=<v1>
applied_before_scale=<bool> piece_scale=<s>
world_delta_mm=<d> lever_full_range_mm=<r>
That last field is the one that ends the argument. If lever_full_range_mm is smaller than the movement you have been asked for, you are not tuning — you are rounding. Stop and go find the lever that can reach.
There is a sharper version of the same failure, and it has its own tell.
A character body was going invisible up close while the goggles and jetpack kept floating in mid-air. I shipped a fix. Same symptom. Shipped a second. Same symptom. Shipped a third. Each time the report came back word for word: exact same range, as if no changes at all.
All three fixes were live. None of them touched the cause.
The renderer was culling the body against a bounding sphere computed in the model's own local units, then never converted to world space — and the models were authored at roughly a hundredth of world scale. The sphere it was testing against was a few centimetres across. The body it was supposed to contain was person-sized. Inflating that sphere three times over, which is what two of my three fixes did, inflated the wrong number three times over.
Here is the distinction worth writing on the wall:
Those two reports feel similar in a message thread. They are completely different diagnoses. The second one is the system telling you your code is not on the path.
And never compare or inflate a length without converting it to world units first, against something you know the size of. A person is about person-sized. If your number does not survive that comparison, it is in the wrong space.
One more way this bites, and it is the meanest one, because it looks like success.
I built frame-time telemetry to chase a performance problem. The report came back clean and specific: median frame time and worst-case frame time, identical to the digit, sitting exactly on a round number. Across the whole sample buffer.
I nearly optimised against it.
That reading was not a measurement. It was a ceiling. The telemetry was sampling a simulation delta that gets clamped upstream to stop physics from tunnelling through walls, so every frame slower than the clamp reported as exactly the clamp. The true statement was "more than ninety-five percent of frames were slower than this." What it looked like was a precise answer.
A distribution with zero variance sitting on a suspiciously round number is an instrument pinned at its limit, not a fact about your program.
A saturated instrument is worse than no instrument, because no instrument keeps you looking and a saturated one ends the investigation with a plausible number.
Three habits prevent it:
max alongside your percentiles, plus an over_X_pct counter. If p50 and p95 are equal, the max tells you instantly whether that is a calm system or a pinned gauge.rig-attachment-fit in the bundle does the first half of this automatically for anything mounted to a character rig — it traces a requested nudge to the world transform, refuses to ship a sub-millimetre move, and reports the delta in mm before you touch a file. The rest of the chapter you can apply by hand to anything.
05
Two outages went out to a live game in a short span. Both shipped behind a green check.
One. A boot hang — the loading screen sat there and never handed over. I reasoned — correctly — that a failed asset request cannot stall the engine's loading manager, and concluded the fault must be a request stuck pending. It wasn't. It was a plain synchronous error thrown in the init function immediately after loading finished. Right reasoning, wrong side of the boundary.
Two. A black screen, with the audio still playing underneath it. Fixing outage one, I ran a scope check on the helper involved, confirmed the function was declared where I thought, and shipped. The new bug was declaration order inside that function — a const referenced above its own declaration, throwing on every single frame. My check proved the code existed. It proved nothing about when it ran.
A quieter version of the same mistake never took anything down at all. I converted a batch of animation clips for a character rig and verified them with a script that read the animation channels inside each file and matched them against the skeleton. It reported every joint mapped. Clean. Green. Shipped.
In the glTF format, a node is only marked as a bone when a skin actually references it. My script had inspected the animation targets, which existed, and never checked for the skin, which didn't. The files contained zero bones. Every clip silently failed to retarget in the game. My check had verified the file format. The game verifies through a loader.
Same shape every time: I verified something adjacent to the thing that mattered, then reported it as proof.
The wrong instinct is to believe a green check because it is green.
The cheapest check to write is almost always the one that inspects the artifact rather than the one that runs it. Parsing a file is easy. Loading it the way the consumer loads it is work. So we write the easy one, watch it pass, and file the result under "verified" — a word that quietly promises something the check never did.
It runs the other way too, which is how you learn the rule properly. During a large data operation my losslessness gate reported text had been destroyed. It hadn't. The gate was counting encoded binary as text. The operation was correct the entire time and I nearly reverted a good result on the word of a broken checker.
Verify through the consumer's own loader, not through your own parser. A check that does not exercise the path production takes is not evidence — in either colour.
A character was stuck in its default T-pose. I had a detector for exactly that, watching animation weights and logging whenever they collapsed. A video came in showing the character plainly T-posed — and the detector's ring buffer was empty.
I read that empty buffer as a finding: weights are fine, so the bug must be in the bones. I was one round from searching an entirely unrelated subsystem.
It was a weights bug the whole time. My detector only logged when the collapse lasted longer than a threshold I had picked to avoid false positives. The real collapse window was several times shorter. The instrument could not see the event. That is all the silence ever meant.
Silence from an instrument has two possible causes: the event did not happen, or your instrument cannot see it. Treating silence as a positive finding about something else skips that fork entirely.
A feature that lets a player tether a companion animal reported success. The confirmation message fired. The interaction prompt flipped to its opposite state. The correct identifier was stored. Every observable signal said it worked.
The saved row had a null where the anchor position should have been. The animal had nowhere to stand.
A normaliser had rejected the position object because it was built with two coordinates instead of three, and the missing one arrived as NaN. No throw. No warning. It normalised to null on the line immediately before the success message.
A handler that saves and then announces will announce even when what it saved was thrown away, because the announcement is produced by the code path, not by the stored result. It can never disagree with itself.
After any write, read the row back — from the database, or from the server's own state dump — and assert on the fields. Not the toast. Not the return value. The fields. Same rule, one layer over: a restore point's name is not its contents. Two of mine were labelled as pre-change snapshots and had been taken after the change landed. Grep a restore point for a marker of the thing it is supposed to predate before you trust it to undo anything.
Most fast suites on a large single-file client come down to string checks: does this line exist, is this function present, was this constant updated. They are genuinely useful — they catch typos, deletions, half-applied patches and stale versions, and they run in a second.
They are also blind to whole categories, and it is not a coverage gap you can close by adding more of them:
A string check proves a line exists. It cannot prove the line runs, runs at the right time, or produces the correct output. Those four areas are exactly where the gap is total — and they are exactly where a player notices in the first five seconds.
Two rules follow, and they have held every time since:
Anything in those four categories ships alone. Never stacked with other changes. And it gets reported honestly — "this needs your eyes", not "verified".
Anything visual ships behind a flag. A URL parameter that toggles the new behaviour, with the old path still intact underneath. Reverting is then closing a tab, not a deploy. One ?nocull toggle eliminated my leading suspect in a single page reload, because the person with the eyes could A/B it himself. The same pattern let an entire asset-optimisation pass run as an opt-in experiment against the shipped build instead of replacing it. The game at https://eternalexpanse.com still carries several of those flags.
The bundled parsecheck.mjs is the smallest useful version of this idea: it loads an asset back through the same loader the client uses and reports what the client will actually see, not what the file claims to contain. It ships in the Builder Stack alongside the deploy tooling that enforces the ship-it-alone-behind-a-flag pattern. The habit matters more than the script — but the script is what makes the habit survive a tired Friday.
06
Two bug reports, filed weeks apart, in completely different language.
The first: logging in took long enough that the word used was "frozen." Not slow. Frozen.
The second: the game got choppy at certain camera angles. Turn to face a cluster of buildings and other players, and the frame rate fell apart. Turn away, and it came back.
I filed those as two problems, because they read as two problems. One is a loading problem. One is a rendering problem. Different subsystems, different owners, different fixes.
So I went after the rendering one. Frustum culling. Draw-call batching. Load ordering. Disposing GPU resources that were sticking around. Every one of those is a real technique and every one of them is what you reach for when a scene is heavy. None of them moved the number. One of them came close to reintroducing an invisibility bug I had already fixed once.
Then I stopped guessing and counted triangles in a live scene.
A single character mesh was over 150,000 triangles. The heaviest was near 300,000. A well-optimised game character is 10,000 to 30,000. The models were arriving from an AI generation pipeline that had no idea it was making game assets — it was making models, and it was making them beautifully, at a density meant for a render farm, not a browser.
That single fact explained both tickets at once. Those meshes are enormous as files, so the boot stream was enormous — that is the frozen login. Those meshes are enormous as geometry, so pointing the camera at several of them at once floods the frame — that is the angle-dependent lag. One cause. Two symptoms that happened to surface in two different vocabularies.
The wrong instinct is to trust the vocabulary of the complaint. "Slow to load" sounds like a network or streaming problem. "Choppy when I look that way" sounds like a renderer problem. So the two reports get routed to two subsystems, and each subsystem gets tuned in isolation by someone who never sees the other ticket.
Symptoms are described in the language of where they were noticed. Not the language of where they came from.
When two unrelated-looking symptoms show up together, find the single upstream cause before you tune either one.
A decimation pass across the asset library: 9.9 million triangles down to 1.66 million, across 131 files.
That is the headline, and the headline is the easy part. Quadric edge collapse is well-understood. What cost me rounds was everything the collapse touches that is not the shape.
Here is the one that will bite you, stated as plainly as I can:
A textured mesh has vertices that exist twice at the same point in space, because a texture map has to be cut into flat pieces to lie flat, and at every cut the two sides carry different texture coordinates. That cut is a seam. On one helmet I measured, 138,308 vertices sat on only 76,154 unique positions — 45% of the mesh existed purely to carry a second texture coordinate at a seam.
Four ways to handle that. I shipped the first one:
| Approach | Result |
|---|---|
| Weld vertices by position | Seams are destroyed. The texture smears across every chart border. This shipped. |
| Weld by position + texture coordinate | The two sides of a seam collapse independently and drift apart. The mesh tears open. |
| Split every corner "to be safe" | Preserves real seams, but invents new splits inside intact regions. |
| Topology on position, output split by texture chart | Correct. |
The correct operation runs the collapse decision on position alone — geometry is geometry — and then emits one output vertex per surviving position per texture chart. A chart is a connected component of the position-and-UV graph; a union-find pass over the faces finds them.
A seam must be reproduced, not removed and not invented.
Preserving a discontinuity is not the same thing as introducing one. Splitting everywhere to be safe is its own defect, and it looks almost exactly like the defect it was meant to fix. The same lesson repeats one attribute over: normals must be grouped by position even where the texture coordinates split, or the seam simply moves out of the texture and into the shading, and you get a visible crack along a line where the model is geometrically continuous.
The reduced assets did not replace the shipped ones. They were written to a parallel directory tree at the same relative paths, with a lookup table mapping each original URL to its variant, and a query-string flag on the client that chooses which set to load.
That gives you an A/B you can run on a live game with real players in it. Load the page one way, get the diet set. Load it the other way, get the originals. Two tabs side by side, same account, same scene, judged by eye. And when something looks wrong, the rollback procedure is closing the tab. No deploy, no restore, no downtime. If the reduced set ever earns the default, the promotion is one flag inversion, not a file copy.
That pattern exists because of the round where I did the other thing.
I overwrote a shipped helmet in place with its approved decimated variant. The original was not in backups/, not in the source archive, not anywhere. The project's restore-point tool skips the asset tree on purpose, and says so in its own header: assets are additive. That was true right up until the moment an asset was replaced instead of added. Nothing announced that the safety net no longer covered the work being done. The outcome was not a restore. The outcome was that the helmet was deleted from the game rather than shipped degraded — a real feature, gone, because I saved myself one file copy.
A backup tool's exclusions encode a model of how the project changes. The moment your work violates that model, the tool stops being a backup and nothing tells you.
Before you tune a renderer, or a loader, or anything else that has a satisfying knob on it:
The decimation toolchain, the flag-based rollout, and the six verifiers that catch smeared textures, torn seams, black shading and broken indices are packaged as glb-asset-diet. Every rule in it is a defect that shipped, or nearly shipped, to a game with players in it — you can go look at the game at https://eternalexpanse.com.
07
The decimation tool was approved on two files. I showed both, they looked right, I got a "proceed," and I ran the batch across 131.
Every quantized asset in the batch came out 32,767 times its correct size. Not all 131 files — the ones that stored plain floats were fine. Every one that stored normalized integers was ruined.
The cause is one line of the glTF spec. A mesh can store vertex positions as plain floats, or as 16-bit integers with a normalization flag, where the integer 32767 means 1.0. My decimator read positions with a raw reader. On a normalized file, that reads 32767 and believes it.
Here is the part that matters. Both approved specimens stored positions as plain floats. Neither one could possibly have exposed the bug. There was no version of that pilot, run more carefully, that would have caught it.
And nothing downstream complained. The tool ran without error. The file got smaller. The triangle count was exactly the target. The output loaded. Every check I had agreed with every other check, because they all shared the same reader and therefore the same wrong assumption. It was only caught because one verifier had been written from the spec rather than from the writer's own helper functions — it decoded normalized values, the writer did not, and the disagreement is what surfaced the bug.
The wrong instinct is to read a successful pilot as a confidence measurement. Two for two feels like evidence. It is not evidence about the population. It is evidence about the code path.
A pilot proves the code runs. It says nothing about the parts of the input space your specimens happened to miss.
Before you approve a pilot, ask what a successful pilot could not have tested.
Not "did it work." That question is already answered and it is the wrong question. The right question is: which properties did these two specimens happen to share that the rest of the population does not?
For binary assets that list is short and knowable: encoding, quantization, rigging, primitive count. One survey pass over the whole library would have printed the encoding column and the answer would have been sitting there. For a database migration it is nulls, character sets, row counts, and rows written by a schema version you forgot existed. For a text pipeline it is encoding, line endings, and the file somebody saved out of a word processor. Every domain has its list. Write yours down once.
A bug report is a sample, not an inventory. When the owner of a project sends you a list of broken things, that list enumerates what he looked at. On one round I was handed a short list of warped assets. Sweeping the whole shipped set instead of the named list measured something different: some of the named files had real damage, some were clean, and a larger group he had never opened was as bad or worse — including the ones he would obviously have checked next.
Fixing exactly the named list would have shipped the rest and guaranteed another round of the identical complaint. When a report names N instances of one defect class, measure the whole population on the metric the report implies, then show the ranking and its cost. Let the person judge a policy, not an anecdote.
Match the lever to the damage type, not the damage size. My instinct on visual damage from a compression pass was "compress less." That instinct was measurably wrong. Aggressiveness did not predict damage at all: the most aggressive reduction in the whole set came out clean, and one of the gentlest was ruined. Turning the dial down is the lever for geometric error. Damage to attributes — texture coordinates, normals, skin weights — does not respond to it, because those are not being approximated, they are being reconstructed incorrectly. The lever for that is exclusion: some inputs are hostile to the transform and should ship untouched.
A number from a fresh test account is a lower bound, not a measurement. During an audit I timed a function at 51 ms on a clean QA character and reported it "conclusively ruled out." The next capture from a real, long-lived account measured the same function at roughly 48 seconds. I was wrong by three orders of magnitude, and I had used the word "conclusively."
The reason is structural, not incidental. The cost drivers for that function were wardrobe size, number of other players nearby, and how much had streamed in. A brand-new account sits at the floor of all three. It is the single least representative subject available, which is exactly why it is the one that is always convenient to test with. An instrument reading low in one configuration is not an exoneration. "Ruled out" needs the same evidence bar as "found it."
Before you say "proceed" on anything that will run across a population:
That last line is the whole chapter. You are not trying to be more careful. You are trying to be honest about the shape of what you just proved.
The survey pass, the encoding checks, and the verifiers-written-from-spec are built into glb-asset-diet; the habit of recording each of these failures somewhere the next session will actually find it is session-continuity. Both are in the Builder Stack — but the checklist above works on paper and costs nothing.
08
An agent's memory ends when the chat does. That much everybody knows. What people miss is the second half: everything that survives does so because it was written into a durable home on purpose, and because something read that home before work started. Storage without retrieval is a diary. Nobody reads the diary.
Nothing goes red when that loop fails. That is what makes it the expensive kind of failure — no stack trace, no failing check, nothing to chase. The bill arrives quietly, as rounds spent re-solving problems that were already solved.
The wrong instinct is to treat this as a memory problem — bigger context, longer notes, one giant file with everything in it. A note that becomes a dumping ground bloats every recall and goes stale invisibly, so it stops being trusted, so it stops being read. Most "why did you forget?" moments are not storage failures at all. The thing was written down. It was written in the wrong home, or nothing loaded it.
Knowledge survives only when it is routed to a durable home on purpose, and something reads that home before the work starts.
Different kinds of knowledge need different homes. Route it by what it is:
| What just happened | Where it goes | Why there |
|---|---|---|
| A bug class that cost two or more rounds | The project's CLAUDE.md lock | Loads with the repo whether or not the next session thinks to look |
| That same lesson, but true beyond this project | Also a skill | Skills travel to other projects, other machines, other people |
| A fact about the project's state or your preferences | A memory, plus a one-line index entry | The index is the part that actually loads |
| Anything that MEASURES | A file in the repo (tools/, tools/probes/) | A probe in a scratchpad is deleted with the chat |
| A repeatable multi-step procedure | A skill, with trigger words in its description | So it fires without being named |
| A capability worth sharing or selling | The bundle, then re-run its scrub gate | One source of truth; never hand-edit the packaged copy |
| A new capability's wiring | The authority map, in the same change | Every capability has a server row and a client mirror |
Two rules make the table work. Index it in the same change or it does not exist — an unindexed memory never loads, and a tool absent from the README gets rewritten from scratch. And write the scar, not the rule. "Use worldToLocal" is forgettable. "Six rounds of percentage nudges moved the part less than a millimetre, because the constant being tuned was multiplied downstream by a smaller piece scale" is not. Evidence is what makes a rule survive contact with a future session that thinks it knows better.
At the end of every round — not every session — rescue what the round produced. Anything that measures, seeds, ports or checks goes into the repo. One-off edit scripts can die.
A scratchpad is not storage. It is deleted with the chat, silently, and the thing you lose is always the probe that proved something.
Then update the indexes, re-sync any bundle that packages a skill you edited, re-run the checks against your baseline, and name your deferrals out loud. A named deferral becomes the next session's backlog. A silent omission is a landmine.
One warning: writing the close-out down does not make it happen. That rule was recorded in three separate places and a round still shipped without it. If it matters, it needs a script in the deploy chain that exits non-zero — not a paragraph of good intentions.
session-continuity is the skill that carries the routing table and the close-out gate, so neither depends on remembering to do it.
The machine had 0.13 GB free on a 1 TB drive.
Not "getting full". Full. Nothing left to delete that mattered — no cache, no downloads folder, no forgotten ISO. The space was gone into a place nobody thinks to look: the agent's own transcript store.
The cause is structural. Every subagent fork re-embeds the parent's images. The conversation itself is tiny; the pictures are stored hundreds of times over. Across 224 files that store had reached 183.609 GB.
After the run, the same 224 files held 4.082 GB. 179.5 GB reclaimed. 545,914 base64 payloads removed. Zero words lost. Every file stayed where it was, same name, same line count, still valid JSON. Only the payload substring was replaced, with a short placeholder describing what had been there.
Nothing was thrown away either. Recovering the pictures was a separate job, with its own scan and its own count — the 545,914 above is what the compaction pass removed, not what the image pass found. That second scan walked 577,721 embedded copies and found they collapsed onto just 3,739 distinct images, each written once as a real file and referenced by content hash, for 3.26 GB. That mattered more than it sounds. On that machine the agent's own generated-images folder held just 10 files. For everything else, the copy buried inside the transcript was the last copy in existence. A naive cleanup would have deleted the only surviving originals and reported a win.
The wrong instinct is to zip the folder, or delete last quarter's sessions, or move the whole thing to an external drive. All three treat the store as dead weight. It is not dead weight — it is the only searchable record of how everything got built. And the naive version of the smart approach, a regex that finds base64 and cuts it out, is worse than any of them, because it destroys text while reporting success.
Any bulk edit of a line-delimited store needs a byte-level losslessness proof, not a spot check.
Every one of these was hit for real:
Dry-run the whole corpus first. Prove losslessness on one file — line count, JSON parse, an independent byte-level walk across both files on disk, and a text digest that must match. Then apply to derived and subagent files first and irreplaceable human threads last, only after the bulk pass has fully verified. On a full disk, unlink the original before copying the smaller result back, so each file frees headroom for the next. And on any failure, say plainly whether the original is intact or whether the verified copy is now the only copy. Reporting the second as the first is how a recovery becomes a data loss.
transcript-corpus-compaction ships that toolchain — the sampler, the stripper, the vault, and the gates that prove nothing was lost. Both it and session-continuity are in the Builder Stack, for whenever your own drive gets there.
09
The first Short I priced out honestly cost about 210 credits to render. One video. A minute long. I had the script, the voice, and a folder of images, and the only thing standing between that and a published video was motion — slow pans and pushes across still frames. I was paying a hosted generation service, per clip, to produce camera moves that a 1990s slideshow could do.
That is the shape of this chapter. The problem was never that the AI can't make things. It makes things constantly. The gap is between a thing existing and somebody paying for it, and that gap is almost always built out of two materials: steps you are renting that you do not need to rent, and steps you are doing by hand that a script should be doing.
The wrong instinct is to treat the expensive step as the necessary step, and to treat the last mile — the pack, the images, the listing, the upload — as admin work that doesn't deserve engineering. It is the reverse. The last mile is where the money actually changes hands, and it is the most automatable part of the entire pipeline.
Rule: output becomes product at the last mile — find the step you are renting and the step you are hand-doing, and kill them in that order.
Ken Burns motion is a crop rectangle interpolated over time. That's it. ffmpeg does it locally, free, deterministically, in a loop, on hardware you already own. Once the motion moved to a local render script, the cost of a Short went from roughly 210 credits to roughly 5. Same length, same script, same voice, same images. The only thing that changed was which machine drew the camera move.
The general lesson matters more than the video. Every pipeline has a bill, and the bill is rarely itemised where you can see it. So itemise it:
Paying for a model you genuinely cannot run is buying a capability. Paying to interpolate a rectangle is buying convenience, and convenience at forty times the price is a tax you volunteered for.
Marketplace listing images killed more of my afternoons than any code. Open the page, resize the window, screenshot, crop, drop it into a design tool, add a caption, export at the size the marketplace demands, notice the text is wrong, do all of it again.
The fix was to stop treating it as a design task. Headless Chrome, driven over the DevTools protocol, loads any public URL and captures it. A local HTML page acts as a compositor: it lays that capture out with headline text and framing, rendered at the exact pixel dimensions the listing requires. No manual screenshots, no paid tools, nothing installed from npm.
The part that pays for itself is not the first run. It's the tenth. When a caption is wrong, or the page behind it changed, or you want the same shot at a different size, regeneration costs nothing and takes seconds. A screenshot is a manual step wearing a creative disguise. Once it is a script, iteration becomes free, and free iteration is the difference between a listing that looks made and a listing that looks published.
The products themselves are Notion templates, and they are not hand-built either. The builder is a pattern-assembly engine, not one hardcoded template with the words swapped: it composes a full dashboard for a niche from reusable structural patterns, so "ADHD planner" and "fitness coach HQ" are the same machine with different inputs.
Getting one onto a marketplace is the other half. A second skill drives the Etsy listing editor directly — category, title, description, all thirteen tags, price, quantity, the how-it's-made fields — and then deliberately stops at save-as-draft. A human uploads the files and presses publish.
That stop is the design, not a shortcoming. Publishing is irreversible and the listing carries my shop's name on it forever. Automate right up to the last reversible step, then hand over the keys. Every automation you build should have that line drawn in it somewhere, and you should be able to say exactly where it is.
Here is the honest loop, because it is the only reason this chapter has standing: the live listings in my shop at https://www.etsy.com/shop/MrNiceTye — the ADHD and fitness Notion dashboards, plus the invitation packs — were built with the template builder and listed with the Etsy skill. Both of those are in the bundle. The shop is not a mockup of what the skills could do. The shop is the output.
In the bundle, the video pipeline is higgsfield-shorts-pipeline (it ships the local ffmpeg render script), the image compositor is listing-shots, the marketplace driver is etsy-lister, and notion-template-builder makes the thing they sell.
10
Every rule in this book cost something before it became a rule. Most of them cost it twice, because the first time I learned it I didn't write it down anywhere a future session could find it. That is the whole reason these are skills and not blog posts. A rule that lives in your head expires when the session ends. A rule that lives in a skill file gets loaded automatically the next time the work looks like the work that broke.
Fourteen skills, three groups.
game-asset-forge — text idea to a textured, auto-rigged, game-ready GLB. Prevents burning credits on models that look fine and fail the rig contract.glb-asset-diet — the decimation toolchain plus six verifiers. Prevents shipping assets whose heaviest single mesh runs to nearly 300,000 triangles on its own, and then blaming the renderer.live-game-deploy — patching a live game with players online. Prevents the deploy that edits one file of a twin pair and appears to do nothing.mixamo-clip-pipeline — pure-Node FBX to GLB, no Blender. Prevents animations that convert cleanly and silently drop every track.rig-attachment-fit — helmets, props, hair on a rig. Prevents nudging a constant that was never the one controlling the offset.second-life-bento-avatar — rigged avatars and in-world content. Prevents meshes that look correct locally and warp in-world.session-continuity — preflight, routing table, close-out. Prevents rebuilding something you already own and losing the lesson when the chat closes.transcript-corpus-compaction — reclaims the disk from agent transcript stores. Prevents a full drive, and prevents the naive version of the fix that quietly destroys text.higgsfield-shorts-pipeline — end-to-end Shorts and long-form production. Prevents paying per clip for motion you can render locally.reference-video-adaptation — deconstructs a reference video into format levers. Prevents copying the surface of a video that works and missing the structure.video-to-skill — turns a tutorial into a permanent skill. Prevents re-watching the same video every time you need the workflow.listing-shots — any public URL to designed marketing images. Prevents the manual screenshot loop.etsy-lister — pre-fills a full marketplace listing. Prevents the tab-by-tab data entry, and stops before publish.notion-template-builder — builds complete, sellable Notion templates. Prevents one hardcoded template pretending to be a product line.Roughly sixteen zero-dependency scripts ship alongside them — the measuring, verifying, welding, inspecting, patching and stripping tools these rules refer to. They are plain Node. No install step, no package tree, nothing to break in six months.
game-forge: Node, and a browser game to point it at. The decimation, conversion and verification scripts are pure Node and need nothing else. Only asset generation reaches outside — that step needs an image generator and an image-to-3D converter available as tools. Every other skill in the group runs entirely on your machine.
agent-ops: Node, and read/write access to your own transcript folders. No network, no service, no account.
creator-stack: ffmpeg on your PATH for video; Chrome installed for the image compositor; a Notion connection for the template builder; browser control for the marketplace lister. Voice and image generation in the Shorts pipeline use a hosted service — the local render step, the one that saves the money, does not.
The complete Claude Code Builder Stack — all fourteen skills and every bundled script — is $27. The current link lives in the description of every video on the channel: https://youtube.com/@BroadenedHorizon.
Installing it takes one of two shapes. Either you add the bundle as a Claude Code plugin — register its marketplace, then install the plugin from it — and the skills load themselves whenever the work matches. Or you copy the skill directories straight into your own personal skills folder. They are plain files either way. There is no build step, no package tree, and nothing to uninstall except a folder.
That price is low deliberately, and the reason is not a marketing structure. Every rule in this bundle already cost its price. It cost it in generation credits spent on assets that failed a rig contract. It cost it in a two-file pilot approved with confidence and rolled out across 131 files. It cost it in two production outages that every green check in the QA harness failed to see, on a game with real players logged in. That tuition is already paid. The only question left is whether you pay it again yourself, one scar at a time, or read the receipt for the price of lunch.
If you'd rather not buy anything, that is a legitimate outcome of this book. The rules are all in here, stated plainly, and they work whether or not you own the scripts that automate them.
One page. Screenshot it.
This whole book is one argument.
Building is not the hard part any more. You can describe a thing and watch it appear, and the first eighty percent will arrive faster than you can review it. That part is solved, and it is going to keep getting more solved.
Finishing is the part nobody automated. Finishing is the twin file, the verifier that measures the right layer, the pilot wide enough to be a sample, the note you wrote at 2am so the next session doesn't relearn it, and the last irreversible click you deliberately left to a human.
None of that is glamorous, and none of it is hard. It is just the part everyone skips, which is exactly why the folder full of eighty-percent projects is the most common object in this field.
Ship one thing all the way to the end. Then the rules in here stop being someone else's scars and start being yours.