Builder Stack Field guide / 01

SHIPIT.

A field guide to finishing with AI.

Ten failures. Ten useful rules.
A smaller next step you can actually check.

Free, complete guideNo signup requiredExplore the toolkit
The working loopA small result. A real check. A useful next step.
  1. 01ChooseOne outcome you can inspect
  2. 02MakeThe smallest useful version
  3. 03CheckThrough the tool that uses it
  4. 04KeepThe evidence and next step

Who this is for

You know the game you keep wishing someone would make?

I stopped waiting. I could not code. I was a gamer with an idea, and AI helped me turn it into Eternal Expanse: a 3D online world where people can explore, build and play together.

Nineteen days got me into the world I had imagined. They did not finish every feature, solve every bug or produce the whole toolkit in this book. The skills, scripts and specialist workflows grew from making the game and from the work that followed. That distinction matters: the beginning was fast; the useful lessons came from keeping it working.

The first time two characters moved through the same world, the idea became real. Then other people tried it. Things broke in ways I had never thought to test.

This free guide is what I wish I had beside me then: ten failures, their real costs, and the habits that made the next attempt better. You can use those habits with a game, an app, a creative product or a small project you want to finish.

Here from the game? Chapter 1 tells the story. Building something of your own? Start with the short exercise below, then use the chapters when a problem sounds like yours. The technical examples keep their details so you can examine the evidence; you can still use the rule without knowing the geometry.

Start with one useful round

Pick one thing another person could actually try. “Walk into one room and save my progress” is a useful first task. “Build my entire dream game” leaves too much hidden.

  1. Say what done looks like. Describe the result, the available files and the one action a person should be able to take.
  2. Ask what already exists. Have your agent find the relevant project notes, scripts, skills and specialist before creating anything new. Load the owner for this task; a longer instruction pile is not automatically a better one.
  3. Make a fresh working copy. Keep the last version you liked. Change one coherent piece so you can tell what improved.
  4. Try the actual result. Open the file, play the game or watch the exported video in the software your audience will use. A successful build message is only one check.
  5. Save the useful fix. Record what failed, what worked and where the next session should read it. Freeze the version you approve; start the next change from a copy.

That is a complete first round. You do not need to buy anything to do it.

What you'll walk away with

Ten rules, in the order the failures arrive. Read only the bold lines and you have still got most of the book.

  1. Budget for the tail: the last 20% is not harder work, it is a different failure surface — silent instead of loud.
  2. Read what already exists before you write anything, and treat every claim in a handoff as a hypothesis until one grep confirms it.
  3. 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.
  4. On the second failed fix of the same tuning problem, stop changing the value and ship instrumentation instead.
  5. Verify through the consumer's own loader, not your own parser — a check that does not exercise production's path is not evidence, in either colour.
  6. When two unrelated-looking symptoms show up together, find the single upstream cause before you tune either one.
  7. Before you approve a pilot, ask what a successful pilot could not have tested.
  8. Write the scar, not the rule — routed to a durable home in the same change, or you will buy the lesson twice.
  9. 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.
  10. Ship the thing. Nothing on this list matters until someone who isn't you uses it.

Nine of those get a chapter: what broke, what it cost, and something you can run today. The tenth is the closing note, because it is the only one that is not a technique.

Each line above is the exact line its chapter states and the card at the back repeats — one wording, three places, so the summary can be trusted against the book.

Two of them are one skill wearing two hats: rule 2 is what it does before a round, rule 8 is what it does after. I say so where they meet rather than counting it twice.

What this is not

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. The short code blocks support the examples: a preflight checklist, a file layout, a log format, a deploy sequence and one five-line comparison script. Use the examples with your own project’s paths and permissions.

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.

How to read this

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. Chapter 10 is the exception: it is the offer, and you can skip it without losing a rule.

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 for the full story, or use it as a reference. Chapter 2 helps you start a round; Chapter 8 helps you keep the lesson; the updated story-ad method in Chapter 9 applies the same loop to a finished video. Chapter 10 separates the available product groups from the wider working setup.

The 80% Trap

What broke

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 proved the world could run at 1.66 million triangles instead of 9.9 million, across 131 files, because the models that made the game look good were the reason it took forever to load. (It runs behind a flag rather than as the served default, and Chapter 6 is why that was the right call.)

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 has taken every week since, and it is not finished.

The wrong instinct

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.

Budget for the tail: the last 20% is not harder work, it is a different failure surface — silent instead of loud.

Almost nobody writes that one down.

How to apply it

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.

  1. What is the silent failure? Name the way this breaks that produces no error, no log line, and no crash. If you cannot name one, you have not looked; every system has one.
  2. Who is the first person that is not me? Name them. A specific person with a specific machine, on a connection you do not control.
  3. What does my check actually measure? Not what it is called. Trace it to the layer the user's software runs on. A test that reads your source file has not tested your product. (Chapter 5.)
  4. What is my rollback, and have I run it? Not "there is a backup." Have you restored from it, on purpose, while nothing was wrong?
  5. What did this project already teach me that I have forgotten? Search your own notes before you build. The most expensive bugs in this book are the ones I solved twice. (Chapter 2.)

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. Many of the repeated checks became something a tool could run, because a rule you have to remember is easy to skip at 2 a.m. Those became reusable skills and scripts, with specialists to route the right task to the right instructions. The working setup has continued to expand. I name relevant workflows throughout; Chapter 10 explains the product groups and their boundaries. This chapter's rule does not have a skill. It has the other nine chapters.

The stakes

I can make all of this abstract and it would still sound reasonable. So here is the only part that is not an argument.

You can try Eternal Expanse at https://eternalexpanse.com: hit Play, make a character and walk around. It has had real players. That does not mean someone else will be online at the moment you arrive.

That public, playable world is the proof behind this book. The game continues to change; the numbers in the chapters are records of the particular build or incident being discussed, not today’s player count, performance or feature inventory.

Everything in the next nine chapters is what it cost to be able to say that sentence — and all of it is what I would tell you, unprompted and at length, if you said you were starting yours this weekend.

Build Notes

Stopping here, or saving it for later?

Either is fine — the whole book is on this page, free, and nothing in it moves behind an email address. This is only for what comes after it. When the next thing breaks and earns a rule, Build Notes shares the lesson and the check that would have caught it.

Join Build Notes

A five-email introduction, then occasional Builder Stack notes and relevant product offers. Unsubscribe any time. Opens the MailerLite signup page in a new tab. SHIP IT stays free without joining.

Preflight: Check Before You Build

I wrote a CDN edge-warmer from scratch.

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.

The wrong instinct

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.

The rule

Read what already exists before you write anything, and treat every claim in a handoff as a hypothesis until one grep confirms it.

The preflight

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. Find the existing owner and tooling. If your setup has specialists, name the one for this task and read its instructions. A specialist routes the work; a skill explains the method; a script performs a repeatable step. Reuse those before writing another version. Then list the tools. 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 check that vanished. A suite going 86 to 86 while one check silently disappeared and another was added looks perfectly healthy in a pass count.
  • A silently truncated file. Scripted edits remove more than intended more often than anyone admits, and byte counts and row counts both move when they do.

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
[ ] Find the relevant skill / specialist; list tools/ and scripts/ before adding one
[ ] 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 wrote that negative case, for a file-corruption guard, the guard failed its own test on the first run — it caught one flavour of mojibake and missed another entirely, because only one of the two bad decodes produces the familiar garbage. Elsewhere in the same project, a grep check asserting the text of a fix stayed green for four versions while the code it guarded computed in the wrong coordinate space and did nothing. Neither gate was lying. Neither had ever been asked a question it could fail.

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.

The Twin-File Rule

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.

The shape of it

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

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 rule

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.

How to actually ship one

Put this in a file called hashcheck.cjs. Not in a shell one-liner — quoting rules differ between bash, PowerShell and cmd, and a gate that only runs on your terminal is a gate you will skip on someone else's.

const fs = require('fs'), c = require('crypto');
const h = p => c.createHash('sha256').update(fs.readFileSync(p)).digest('hex').slice(0, 16);
const [a, b] = process.argv.slice(2);
console.log(h(a), h(b), h(a) === h(b) ? 'MATCH' : 'DIFFERENT — the deploy is not done');
process.exit(h(a) === h(b) ? 0 : 1);

Then node hashcheck.cjs game.js public/game.js. Two hashes. They match or the deploy is not done, and the exit code says so to whatever is running it. 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.

CDN warming: the easiest false green there is

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:

  • The extension. The edge caches by URL extension and never caches a bare .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.
  • The token. Per-file cache tokens ride in the query string, and the edge caches the full URL including the query. Warm /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

Do this today

  • Find your twin. Every deployed project has one — a build output, a dist/, a copied static tree, a container image. Name the file you edit and the file that is actually read.
  • Write hashcheck.cjs above and put it in the deploy path, not in your head.
  • Run your normal deploy, then hash both. If you have never done this, do it before you change anything: about a third of the time the answer is interesting.
  • Grep the served response over HTTP for a marker of your change. Disk state is where the bug lives.
  • Before you trust any warmer, cache or CDN step, print the header that proves the caching happened. A status code is not that header.

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. None of that is the point, though. The point is that a deploy either ends in a comparison or it ends in a hope, and you can have the comparison for five lines.

Measure, Don't Nudge

A hairstyle sat too low on a character's head. Cosmetic. Obvious. A five-minute fix.

I raised it 20%. The report came back: it hasn't moved. I raised it 35%. Hasn't moved.

  1. 45. 50. 55. 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 wrong instinct

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 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.

Trace to the value that actually reaches the output

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.

"As if nothing changed at all" means wrong subsystem

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:

  • "It moved, but not enough" → your magnitude is wrong. Tune it.
  • "As if nothing changed at all" → your subsystem is wrong. Stop tuning and go find which mechanism actually fires.

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.

When the instrument is pinned, not measuring

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. Note what the report could and could not support. Median equal to worst case equal to the clamp tells you that at least half the frames were at or above that number — and nothing whatsoever about how far above, which is the only quantity I actually needed. 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:

  1. Before you trust new telemetry, feed it a synthetic extreme — force one slow frame, one huge file, one stalled request — and confirm the number moves.
  2. Emit a saturation counter beside every percentile — at_limit_pct, the share of samples sitting exactly on the boundary. max does not save you here: when the clamp is upstream, the max is pinned too, which is precisely how this one passed. A healthy system reports at_limit_pct: 0 and the argument is over in one line.
  3. Never sample a value that something else has already clamped, smoothed, or averaged. Measure the raw thing, then clamp for use.

Do this today

  • Pick the bug you have "fixed" twice. Stop fixing it.
  • Make the running code print, on every attempt: the target, the actual, the delta in real units, and where the target came from.
  • Add the full-range line — how far can this constant possibly move the output.
  • Ship the instrumentation as the deliverable for that round. Ask for one pasted line.
  • If the delta your own arithmetic produces is smaller than the change you were asked for, do not ship it. You already know it will fail.

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. But the script is only enforcing something you can decide on your own this afternoon: that the second failure buys an instrument, not another guess.

Verify the Right Layer

The loading screen sat there and never handed over.

That was the first of two outages that went out to a live game in a short span, both of them behind a green check.

One. The boot hang. 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.

Here is what they cost, as precisely as I can state it. Both stayed hidden for hours after they were live — not because the browser was silent, but because the console's level filter hides Verbose by default and that is where both exceptions were sitting. The deeper bill was the harness itself: its debug login entered the world as a different avatar class than a real player, so a key asset map was empty on every build, and both outages sailed straight through it while it reported green. And I do not know how many people loaded the game during either window and saw nothing. That I cannot answer it is its own indictment — I had no instrument pointed at the one thing the whole product is for.

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

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 your own parser — a check that does not exercise production's path is not evidence, in either colour.

Chapter 3's rule is this one at a different altitude: there, the layer you skipped was which file; here it is which reader. If you catch yourself saying "the check passed," finish the sentence with the layer. The passing is never the interesting part.

An empty detector is not evidence

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.

  • Before drawing any conclusion from an empty detector, state out loud what it cannot see — its threshold, its sample rate, its gating conditions, whether it sits above or below an early return.
  • Prefer detectors that always emit something. Log the most extreme value seen per window, or the near-misses. Then "nothing happened" and "I couldn't see it" are two different outputs instead of the same blank.
  • Measure the symptom itself, not a proxy for it, whenever you can.

Read the row that persists, not the message that acknowledges it

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.

Grep suites are structurally blind

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:

  • Construction order — whether an object is fully assembled before it is used.
  • Boot sequence — whether the line runs, and runs at a safe moment.
  • Streaming — whether an asset actually arrives, and what happens while it hasn't.
  • Culling and visibility — whether the right pixels reach the screen.

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.

Do this today

  • Take your most trusted green check. Write down, in one sentence, what it does not cover. If you cannot, it is not a check, it is a habit.
  • Replace one format-level check with a loader-level one — load the artifact through the same code path production uses, and assert on what comes out the other side.
  • After your next write operation, read the row back and assert on the fields.
  • Put a flag on your next visual change before you put it in a release.

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.

The next chapter changes altitude. Two bug reports, no philosophy, and one counted number that made every renderer fix I had shipped look like decoration.

Build Notes · halfway

These keep happening. That is the whole point.

Five chapters, five things that broke. The book stops at ten because a book has to stop; the project didn’t. When a new lesson earns a note, it follows the same three questions — what broke, what it cost, and what would catch it next time.

Join Build Notes

A five-email introduction, then occasional Builder Stack notes and relevant product offers. Unsubscribe any time. Opens the MailerLite signup page in a new tab. SHIP IT stays free without joining.

The Assets Are the Problem

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

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.

What the fix actually was

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:

ApproachResult
Weld vertices by positionSeams are destroyed. The texture smears across every chart border. This shipped.
Weld by position + texture coordinateThe 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 chartCorrect.

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.

Roll it out so that reverting costs nothing

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.

Do this today

Before you tune a renderer, or a loader, or anything else that has a satisfying knob on it:

  • Count the actual triangles in a live scene, not in the editor and not in the source files. Count only meshes whose entire ancestor chain is visible — hidden geometry inflates the naive total badly.
  • Compare against the budget for the thing. A character is 10k–30k. If yours is 150k, stop reading performance articles.
  • Write down every open performance complaint on one page and ask whether one upstream fact explains all of them.
  • Copy any asset you are about to overwrite somewhere out of the way first. One copy command is the entire cost.
  • Ship the reduction behind a flag that loads a parallel path, so reverting is a tab close and not a deploy.
  • Verify the attributes, not just the shape. Surface deviation on that smeared helmet read a healthy 0.32 mm the whole time. The shape was always right. The texturing was wrong, and the shape verifier is structurally incapable of noticing.

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. But the first line of that checklist is the one that mattered, and it needs no tooling at all: I spent rounds tuning a renderer because I never counted the thing it was being asked to draw.

Two Specimens Are Not a Sample

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

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.

Three siblings of the same error

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 48.8 seconds. That is a factor of nearly a thousand, 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."

A pilot-sizing checklist

Before you say "proceed" on anything that will run across a population:

  • Name the input space in columns. Encoding, size, schema version, whether it is rigged, whether it is compressed, who authored it, how old it is.
  • Run one survey pass that prints those columns for the whole population. This is cheaper than the pilot itself, almost always.
  • Locate your specimens in that table. Highlight every column where they are identical to each other. Those are the untested columns.
  • Pick the next specimens deliberately from the untested columns. Do not pick the two that are easy to open.
  • Include at least one specimen you expect to fail. A control that cannot fail validates your plumbing, not your metric.
  • Write the verifier from the specification, not from the writer's helper functions. If both sides share a reader they will agree perfectly on a broken file.
  • Batch in tranches with a checkpoint, not in one run. 131 files failing at once and 8 files failing once are the same bug and very different afternoons.
  • Write down, in one sentence, what this pilot cannot tell you. Put it in the approval message.

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.

Keeping What You Learned

Part one: the knowledge

That rule was recorded in three separate places, and a round still shipped without it.

Three. A project lock, a durable note, and the close-out procedure itself — all of them saying the same sentence, none of them consulted, and the round went out anyway. I had solved the retrieval problem by writing more, which is the same as not solving it.

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

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. Which gives the rule:

Write the scar, not the rule — routed to a durable home in the same change, or you will buy the lesson twice.

The routing table

Different kinds of knowledge need different homes. Route it by what it is:

What just happenedWhere it goesWhy there
A bug class that cost two or more roundsThe project's CLAUDE.md lockLoads with the repo whether or not the next session thinks to look
That same lesson, but true beyond this projectAlso a skillSkills travel to other projects, other machines, other people
A fact about the project’s state or your preferencesA project note or permitted memory entry, plus an indexUse the host’s actual permission and storage rules
Anything that MEASURESA file in the repo (tools/, tools/probes/)A probe in a scratchpad is deleted with the chat
A repeatable multi-step procedureA skill, with trigger words in its descriptionSo it fires without being named
A capability worth sharing or sellingThe bundle, then re-run its scrub gateOne source of truth; never hand-edit the packaged copy
A new capability's wiringThe authority map, in the same changeEvery 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.

The close-out

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, and it is the one from the top of the chapter: writing the close-out down does not make it happen. Three copies of a rule are three things nobody read. 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. It is Chapter 2's skill seen from the other end of the round: that chapter is what it reads before you start, this is what it writes when you finish. One loop, two halves, and this is the half people skip.

One addition from the work since the original build: save an approved result as an example, not just as an adjective. “Natural voice” is hard to recover from a note. The exact approved video, its source list and its recipe give the next session something it can compare with. Record the file’s hash — its digital fingerprint — and the approval, then keep that reference unchanged. Later revisions get new folders.

The working setup uses one canonical skill for a method and small specialist pointers to it. Copying the whole method into every agent profile creates several versions to maintain. Sharing a package adds another boundary: ship the reusable instructions and permitted tools; leave private footage, account settings and separately licensed music out. Check what is actually in the extracted package before calling it ready.

The second half of this chapter is the same failure with a weight, because that store has a size and nobody looks at it either.

Part two: the disk

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 about the disk

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.

What actually goes wrong

Every one of these was hit for real:

  • Single lines reach tens of megabytes. Never slurp a file; never blind-parse a line. Cap line length before parsing or the job appears to hang.
  • The regex engine has a stack limit. A match spanning a multi-megabyte string throws before it matches anything, even for a trivial character class. Scan byte indices instead. UTF-8 is self-synchronising, so a byte scan copies everything outside a payload through untouched.
  • There is never only one payload shape. Sample the file before writing the matcher. A matcher built on the obvious shape can miss most of the bytes.
  • The index tool lied. It reported a file count and a size that disk did not agree with. Reconcile the index against disk before and after; do not report from it.
  • Some source files were already truncated before any of this ran. Never hold the output to a higher standard than the input — re-parse the source line and fail only if your edit is what broke it.
  • Verify the verifier. In that run, both failures were in the checker, not the stripper. A failing gate is not proof the work is wrong. Ask what the check measures before you believe what it says.

The order that keeps it safe

Treat the figures above as a record of that recovery, not permission to modify your own history. Name the exact folders and backups first, then use the established tool’s dry run. Prove losslessness on a copied specimen: line count, JSON parse, an independent byte-level comparison and a matching text digest. The archive of retained images must be verified too. Do not proceed past an ambiguous truncation or corruption finding.

In the original full-disk recovery, the procedure sometimes had to free an original before copying its verified smaller replacement back. That was a constrained recovery step, not a general cleanup instruction. Follow your tool’s current backup and replacement gates, and do not delete the only verified copy. On any failure, report exactly which original, backup or replacement remains intact.

Do this today

Both halves of this chapter, and neither needs a script:

  • Take the last lesson that cost you more than one round. Write it down as the scar — what you did, what it cost, what the number was — not as the advice. Put it where the tool loads it, not where you would look for it.
  • Index it in the same change. An unindexed note has never loaded once.
  • Name one thing in your current scratchpad that measures something. Move it into the repo now. That is the file you will want and not have.
  • Then check the size of your agent's session folder. Right now, one command. Most people have never looked, and the number is not usually small.

transcript-corpus-compaction ships the toolchain for that last one — 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.

Turning Output Into Product

The first Short I priced out honestly cost about 210 credits to render at the time. 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.

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.

Worked example one — the rented step (video)

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. Those are the prices from that production run, not a current quote for every video.

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:

  • Write the pipeline out as a list of steps, one line each.
  • Put the per-run cost beside every step.
  • Circle the most expensive one.
  • Ask: what is the actual computation happening here?
  • If you can describe that computation in one plain sentence, you are probably renting 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.

The story-ad method, refined

The launch ad taught a different last-mile lesson. Its script and gameplay already worked. The voice drifted after a strong opening, the music was too sleepy, and some text changed before you could comfortably read it. Making every part slower did not solve that. It made the voice feel drawn out.

The approved revision kept what worked and repaired the specific weak parts. This is the repeatable method we kept from it:

  1. Start with a recognizable wish. Move from the viewer’s wish to a visible result, concrete things they can do, the obstacle you faced and one clear next action. Show your own proof. A new topic does not inherit another project’s nineteen-day story or multiplayer claim.
  2. Use the footage you have. Plan short statement cards beside large, clear media. Keep the same visual hierarchy through the last card. Choose a clean source range or crop when a recording contains a defect; do not make the subject tiny to hide it.
  3. Keep one human pace throughout. In this repair, the approved opening stayed intact and the later narration was remade in compact, coherent sections with the same existing voice. Repeating the opening as a text primer helped set the delivery; that repeated text was removed from each later take before assembly. It was not a new voice clone. Keep natural word lengths and brief thought pauses. Re-voice a stale section instead of slowing the entire track.
  4. Give the eye time without stretching the voice. This cut used statement holds of at least four seconds and image holds of at least 1.8 seconds. Those are starting points for this format. Combine cards if there is too much text; do not add long empty pauses to make a crowded timeline fit.
  5. Give the beat a supporting role. An energetic, licensed trap instrumental kept the rhythm; the narrator stayed clearly above it. Use original or properly licensed music. Slightly changing another creator’s copied beat does not resolve its rights.
  6. Check the export, then approve it. Read back the full narration, inspect every shot and listen through the final file. Check joins, levels and the ending. Save the exact accepted file and recipe. Technical checks and creative approval answer different questions; neither is evidence that an ad will sell.

The accepted ad ran about sixty-two seconds. Its length was the result of the story, not a rule for every future video. The reusable pipeline now includes the section assembly and timing helper; its portable package does not include our private voice recordings, footage or raw licensed soundtrack.

Try it on a smaller subject first: one useful result you can show, one sentence about how you reached it and one action the viewer can take. That gives you something real to refine before paying to generate a folder of new media.

Worked example two — the hand-done step (listing images)

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.

Worked example three — the whole loop (products)

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. Publication puts the listing in front of customers under my shop’s name. In this workflow, the final review and publish action belongs to the owner. Draw that boundary for each automation and follow the authorization for the actual task.

Here is the honest loop, because it is the only reason this chapter has standing: in the shop used for this project, the ADHD and fitness Notion dashboards were built with the template builder, and every listing in the shop — those two plus three invitation packs that came from somewhere else entirely — was filled in by the Etsy skill. Both of those skills are in the bundle. The shop is not a mockup of what they could do. The shop is the output.

What to do this week

  1. Write out one pipeline you run repeatedly. Steps, one per line.
  2. Mark each step: rented, hand-done, or free.
  3. Kill the most expensive rented step first. Ask what the computation is.
  4. Kill the hand-done step that you repeat most often, not the one that takes longest.
  5. Draw the stop line: name the last irreversible action and leave it to a human.

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. Those are four answers to a question you have to ask yourself first, though, and the question is free: write your pipeline out as a list, and put a price next to every line.

The Stack

The free book gives you the habits. The Builder Stack packages reusable instructions and helpers for applying them. It grew from the game and the projects that followed; it was not all built during those first nineteen days.

This September 2026 edition brings together seventeen workflow skills and twenty specialist role contracts, with an index for choosing the right role. The workflows come from Game Forge, agent-ops, Creator Stack and three dedicated workbenches. A skill explains the method; a script carries out a step; a specialist contract defines the task, limits and evidence to return. Your host, tools and permissions determine what can actually run.

The twenty contracts are portable instructions, with a dependency map showing which workflows are included and which resources you supply. They do not automatically start twenty agents or install a global rule-enforcement system. Six general roles have no bundled private workflow implementation. Personal histories, account bindings, approved reference assets and private health, finance or business material stay out.

game-forge — building and shipping a live 3D browser game

  • 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.

agent-ops — operating an AI agent workstation

  • 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.

creator-stack — turning AI output into sellable product

  • higgsfield-shorts-pipeline — Shorts and long-form production, including the refined story-ad method and narration/timing helper. Reuses footage and local rendering where they fit.
  • 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.

Three workbenches — model, rig, animate

  • model-creation-workbench — dimensioned static models and explicit LOD preparation, with guidance for physical texture scale and target delivery. Its small procedural engine supports its documented shapes and materials, not every model you can imagine.
  • rigging-workbench — prepared-scene skin transfer and deformation checks using your permitted reference skeleton/body. It does not infer a correct rig from any mesh.
  • animation-workbench — native Second Life animation from explicit motion tracks, with decoded checks, schematic previews and playback guidance. Arbitrary donor retargeting is a separate workflow.

Supporting scripts handle measuring, inspecting, conversion, local rendering and verification. The extracted package inventory names the actual files. Seventeen counts the workflow skills; twenty counts the role contracts. The routing index is listed separately, rather than counted as another production tool.

What you need connected

game-forge: many inspection and geometry helpers run locally with Node. Read the specific skill's setup before using it: asset generation needs compatible generation tools, animation conversion may need the named loader dependencies, and Second Life work can require Blender, Python and the target viewer. Game hosting and deployment need your own environment and authorization.

agent-ops: the transcript tools use Node and access to the particular folders you authorize. Check the host's current transcript format, backups and supported memory rules. Begin with inspection and the tool's dry-run procedure.

creator-stack: video work uses FFmpeg and, for some assembly scripts, PowerShell; the story-voice helper uses Node and FFmpeg. Listing images use Chrome. Template and marketplace work need the appropriate Notion or browser connection. Hosted voice, image and motion generation can incur separate fees. Raw soundtrack files, private voice takes and game footage are not part of the bundle; use media you have rights to.

workbenches and specialists: read the selected workbench's setup and dependency map. Local installers use Node; model and rig operations need compatible Blender and prepared inputs; the native animation example uses Python. The optional local planner needs your own installed model. MCP connections and other plugins must be connected in your account. A file package does not transfer access, credits or licenses.

The offer

The Claude Code Builder Stack brings together the included workflows, specialist contracts and listed scripts. Service subscriptions, generation credits and separately licensed media are not included.

The free guide remains available without a purchase or signup. Explore the Builder Stack for current pricing, availability and included tools. Join Build Notes for an optional five-email introduction, occasional practical lessons and relevant product offers.

For installation, follow the included README for your supported host. The Claude Code package uses a plugin marketplace; individual skill folders are also plain files that a compatible host can read. That does not make every dependency installed or every connection authorized. Start with one local task and check its result before using a tool on live files.

The reason to use the toolkit is practical: spend less of the next round rediscovering what broke in the last one. The two-file pilot, the two outages and the paid generation mistakes are examples of why these checks exist. They are not a promise that your project will be finished in nineteen days, attract an audience or earn money.

Keep using the book either way. Its rules and exercises stand on their own.

The Ten Rules

One page. Screenshot it. Four movements: 1–2 before you build, 3–5 while you ship, 6–7 what you ship, 8–10 what you keep and what it is all for.

  1. Budget for the tail: the last 20% is not harder work, it is a different failure surface — silent instead of loud.
  2. Read what already exists before you write anything, and treat every claim in a handoff as a hypothesis until one grep confirms it.
  3. 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.
  4. On the second failed fix of the same tuning problem, stop changing the value and ship instrumentation instead.
  5. Verify through the consumer's own loader, not your own parser — a check that does not exercise production's path is not evidence, in either colour.
  6. When two unrelated-looking symptoms show up together, find the single upstream cause before you tune either one.
  7. Before you approve a pilot, ask what a successful pilot could not have tested.
  8. Write the scar, not the rule — routed to a durable home in the same change, or you will buy the lesson twice.
  9. 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.
  10. Ship the thing. Nothing on this list matters until someone who isn't you uses it.

And the eleventh, which the book earned late and never got a chapter: anything visual, or anything in the boot order, ships alone and behind a flag.

From SHIP IT — the ten failures behind https://eternalexpanse.com. The book is free.

Closing note

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 80% 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 a folder full of 80%-finished projects is the most common object in this field.

Ship the thing. Nothing on this list matters until someone who isn't you uses it.

Ship one thing all the way to the end. Then the rules in here stop being someone else's scars and start being yours.

Build Notes · keep going

That is the ten. There will be more.

You read the whole thing and paid nothing for it, which was the deal. Build Notes carries the lessons that come after these ten and useful updates to Builder Stack. The toolkit is available now through Gumroad; joining this list is optional.

Join Build Notes

A five-email introduction, then occasional Builder Stack notes and relevant product offers. Unsubscribe any time. Opens the MailerLite signup page in a new tab. SHIP IT stays free without joining.

Where to go next

  • See the proof, live. Eternal Expanse is a multiplayer 3D browser game running on the stack described in this book. Hit Play and make a character: https://eternalexpanse.com
  • Explore the toolkit. Explore the Builder Stack for the seventeen workflows, twenty specialist contracts, current pricing and availability.
  • Keep building. Join Build Notes for an optional five-email introduction, occasional practical lessons and relevant product offers. The guide and toolkit work independently of the newsletter.
  • Try the creator workflow. Map one repeated task, then use the Chapter 9 checklist to choose what to automate.