---
title: "Ten Times Zero Is Still Zero"
subtitle: >
  "Vibe coder" gets used as an insult: prompt and pray, ship a house of cards. I claim it anyway,
  with one word in front: professional. The receipt is the portfolio you're reading this on, where
  nearly half of everything written is words rather than code.
author: "Tjakoen Stolk"
status: PUBLISHED
type: note
date: 2026-08-07
readingTime: "~18 min"
deck: "Ten times zero | Talk | /talks/ten-times-zero | 34 slides · runs in the browser, no download"
tags: [ai, vibe-coding, workflow, fundamentals, native-first, batch, grain]
summary: >
  How I actually work with AI, and why it holds up. AI is a multiplier, not an addend; it
  multiplies what you already are, and ten times zero is still zero, so I made sure I wasn't zero
  first. Then the receipt, pulled from this portfolio's own git history: a snapshot on 2026-08-22
  counted 503 commits, each co-authored with an AI. Professional vibe coding is the human
  writing the rails and the AI writing much of the code inside them. Includes the playbook, the
  context and loop engineering underneath it, what to check before anything counts as done, and
  where to start if you are early.
---

> **One of my students?** If what you actually want to know is how I feel about *you* using AI, I
> wrote that post for you: [I Nearly Quit Teaching, So I Automated the Part That Was Killing Me](why-i-teach.md).
> Read that first; this one is the belief underneath it.

> **There's a talk version of this**, built for a room full of students, and it's live:
> [tjakoen.github.io/talks/ten-times-zero](/talks/ten-times-zero).
> Same argument, thirty-four slides, and the figures are real rather than pictures of figures. Drag
> the multiplier to zero, run six sessions with and without the loop and watch the bill arrive, or
> open a cold session with no brief and see what it does first. Arrow keys move it.

## "Vibe coder" is used as an insult

You know the tone. *Vibe coder*: someone who types a wish into a chat box, pastes whatever comes
back, and ships a house of cards they couldn't explain if you held their coffee hostage. Prompt and
pray. No tests, no idea, no receipts.

I call myself one anyway. I just add a word in front: *professional.* And because "professional" is
the kind of claim that should show up with evidence and not just vibes, I did the one thing the
label almost never does: I counted.

But before the numbers, the belief they're proving. Because the reason I'm *allowed* to let a
machine do the typing fits on a napkin.

## AI is a multiplier, not an addend

> AI doesn't *add* to what you can do. It *multiplies* it. And ten times zero is still zero.

Give a strong developer an AI and you get a frighteningly fast strong developer. Give a beginner the
same AI and you get a beginner who ships bugs faster and can't tell which ones they are. Same tool.
Wildly different results, because the tool multiplied what was already there.

That's why "just use AI" is terrible advice for a beginner, and "never use AI" is terrible advice for
a professional. The honest version is in the middle and it has an order of operations: **become worth
multiplying first.**

<!-- The static SVG is what a reader is SERVED, and what the export, a printout and a
     no-JS visit keep. site/figures.js swaps in the live version from the talk if it runs. -->
<div class="live-fig" data-live-figure="multiplier" data-surface="figure:multiplier">
<svg viewBox="0 0 620 200" width="100%" role="img"
     aria-label="The multiplier. A skilled developer's small baseline times AI becomes a long bar; a beginner's near-zero baseline times the same AI stays near zero. The AI is identical in both rows; the baseline is the variable."
     style="max-width:560px;height:auto;font-family:Georgia,'Times New Roman',serif;--paper:#faf7f1;--edge:#e6ddd0;--ink:#2b2b2b;--muted:#6b6259;--bar:#cbc1b3;--accent:#d97757"
     xmlns="http://www.w3.org/2000/svg">
  <rect x="0.5" y="0.5" width="619" height="199" style="fill:var(--paper);stroke:var(--edge)"/>
  <text x="28" y="30" style="fill:var(--muted);font-size:15px">The multiplier: the AI is the same, you are not</text>
  <text x="120" y="56" style="fill:var(--muted);font-size:12.5px">what you bring</text>
  <text x="270" y="56" style="fill:var(--muted);font-size:12.5px">× the same AI =</text>
  <text x="28" y="88" style="fill:var(--ink);font-size:14px">Skilled dev</text>
  <rect x="120" y="74" width="90" height="18" style="fill:var(--ink)"/>
  <rect x="270" y="74" width="270" height="18" style="fill:var(--ink)"/>
  <text x="548" y="88" style="fill:var(--muted);font-size:12.5px">a lot, fast</text>
  <text x="28" y="132" style="fill:var(--ink);font-size:14px">Beginner</text>
  <rect x="120" y="118" width="10" height="18" style="fill:var(--bar)"/>
  <rect x="270" y="118" width="30" height="18" style="fill:var(--bar)"/>
  <text x="308" y="132" style="fill:var(--muted);font-size:12.5px">still almost nothing</text>
  <text x="28" y="182" style="fill:var(--accent);font-size:13px">Same AI both rows. You are the variable.</text>
</svg>
</div>
<p class="live-fig__note">Drag it. Same figure as the one in the talk, and the same law: the AI is 10 in every row, and the only thing that changes is what you brought.</p>

*Take it to zero. The output does not get smaller, it disappears.*

## I was the zero, for years

Here's the part I don't usually lead with. For most of my twenties I was a talented amateur with just
enough instinct to talk my way into rooms I had no business being in. Coding in school came easy, so I
coasted on easy. Then when I decided the real value was in business, I kept running the same play: Say
yes to running the thing before I actually knew how to run the thing.

It worked. And that's the trap. It worked just enough to keep me doing it, but never enough to get me what
I was actually after. I helped grow my family's little beachside campsite into a real resort, then a
pandemic flattened it. I ran marketing for a company until I learned that I can't stand doing marketing for
anyone but myself. I started a cafe with friends, a salon with friends, a marketing firm on my own.
One broke even, one sold at break-even, one went nowhere, and it turns out you can't tell your friends
what to do. Then I built and launched an events platform, got it to a real product with
real money moving, and then watched it end badly.

Add it all up and it cost me real money and a few years I can't get back. But through it all, I don't regret a moment of it,
because it taught me the one thing every single failure had in common. It wasn't the ideas. It
was me. I kept starting things I wasn't the right person to start yet.

> You have to be the right person before you try the thing. I paid the tuition to learn that so you don't have to.

So I did the unglamorous thing. I stopped chasing the next launch and started working on the one
variable I could actually fix: me. I slowed down. I went back and learned the foundations properly, not just enough to pass, and I shoved myself into the parts I was worst at, like standing in
front of a room full of students or doing talks on a stage. That's the actual plan: work my butt off, grind, so that by my thirties I'd get to a point where the work I do will be something I picked, on my terms.

That, to me, is the definition of the word professional. Not the title, but the journey I had to take to be able to write this in the first place.

## So we're clear who's talking

I wrote that backstory to buy one sentence: I could have written every line of this
project by hand. I run a team of developers for a living, I teach software engineering
part time using a [platform I built](why-i-teach.md), and I built the thing serving you this very sentence
on a framework and stack I built from the ground up. 

**As a side note:** going frameworkless wasn't ignorance of the
alternatives. I lived inside frameworks for years, but I walked away for my own projects because I learned that [the platform the web is written on grew up while we weren't looking](the-browser-grew-up.md), and to be honest, I'd rather build on the thing
than on the thing on top of the thing.

None of that is there to impress you. It's there to establish me as a multiplier and to reinforce that: A beginner vibe
codes because they *can't* write the code. I vibe code because I *can*. I've learned the typing
was never the valuable part. The value is knowing what to build, how it should be shaped, and when
the machine has quietly done something dumb, catch it in the act and correct. I've made those same
mistakes by hand, expensively, enough times to smell them coming.

So when I hand the typing to a machine, I'm not skipping the hard part. I did the hard part years ago,
the boring way and the expensive way.

While I'm being honest: I should probably put in the record that I don't hold a single credential in AI. I stay updated in trends, I follow the right youtubers and journalists. But what I've got
instead is a long list of ways I've been wrong, and I personally think that this has taught me more than any syllabus
would, I still fully intend to go earn the real thing. But for now we'll just chalk that up as the next foundation I haven't laid yet.

## A decade of rehearsal, billed as management

Directing an AI isn't a new skill I had to go learn. It's the skill my whole twenties turned out to be building: the ventures
that failed, the team I run, the classroom. I'd been rehearsing for a decade. I just didn't know what for.

Think about what a dev manager actually does: hand work to people whose hands you don't control, give
them enough context to succeed, then read what comes back and catch the problems before they ship. A
tech lead sets the conventions and keeps a room full of people pulling the same direction. An educator
explains a thing clearly enough that someone with zero context can do it, then checks their work and
corrects the *misunderstanding*, not just the wrong answer. Now describe running an AI. It's the same
list, top to bottom.

So I stopped treating the model like a magic oracle or a fancy search box, and started treating it
like what it actually resembles: a fast, capable, occasionally overconfident junior on my team, or a
student rushing through an activity for the sake of getting it done. The instant I framed it that way I got better at it overnight, because I already knew how to
do *that.*

<div class="live-fig" data-live-figure="matrix" data-surface="figure:matrix">
<svg viewBox="0 0 620 250" width="100%" role="img"
     aria-label="Four titles, one skill set. A matrix of four roles (dev manager, tech lead, teacher of 150 to 300 students, and directing an AI) running the same moves. Give direction: a clear brief, the conventions, the lesson, a prompt with docs. Verify anyway: review the pull request, the design review, grade the work, read the output. Then all four do the same last move: correct the reasoning, not just the output. Same moves down every column; only the subject changes."
     style="max-width:560px;height:auto;font-family:Georgia,'Times New Roman',serif;--paper:#faf7f1;--edge:#e6ddd0;--ink:#2b2b2b;--muted:#6b6259;--bar:#cbc1b3;--accent:#d97757"
     xmlns="http://www.w3.org/2000/svg">
  <rect x="0.5" y="0.5" width="619" height="249" style="fill:var(--paper);stroke:var(--edge)"/>
  <text x="28" y="30" style="fill:var(--muted);font-size:15px">Four titles, one skill set</text>
  <line x1="150" y1="44" x2="150" y2="152" style="stroke:var(--edge);stroke-width:1"/>
  <line x1="261.5" y1="44" x2="261.5" y2="152" style="stroke:var(--edge);stroke-width:1"/>
  <line x1="373" y1="44" x2="373" y2="152" style="stroke:var(--edge);stroke-width:1"/>
  <line x1="484.5" y1="44" x2="484.5" y2="152" style="stroke:var(--edge);stroke-width:1"/>
  <text x="206" y="60" text-anchor="middle" style="fill:var(--muted);font-size:13px">Dev manager</text>
  <text x="317" y="60" text-anchor="middle" style="fill:var(--muted);font-size:13px">Tech lead</text>
  <text x="429" y="60" text-anchor="middle" style="fill:var(--muted);font-size:13px">Teacher (150–300)</text>
  <text x="540" y="60" text-anchor="middle" style="fill:var(--muted);font-size:13px">AI</text>
  <line x1="24" y1="72" x2="596" y2="72" style="stroke:var(--edge);stroke-width:1"/>
  <text x="24" y="104" style="fill:var(--ink);font-size:14px">Give direction</text>
  <text x="206" y="104" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">a clear brief</text>
  <text x="317" y="104" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">the conventions</text>
  <text x="429" y="104" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">the lesson</text>
  <text x="540" y="104" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">prompt + docs</text>
  <text x="24" y="140" style="fill:var(--ink);font-size:14px">Verify anyway</text>
  <text x="206" y="140" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">review the PR</text>
  <text x="317" y="140" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">design review</text>
  <text x="429" y="140" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">grade the work</text>
  <text x="540" y="140" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">read the output</text>
  <rect x="24" y="166" width="572" height="30" style="fill:var(--edge)"/>
  <text x="310" y="186" text-anchor="middle" style="fill:var(--ink);font-size:13px">Then all four: correct the reasoning, not just the output</text>
  <text x="28" y="232" style="fill:var(--accent);font-size:13px">Same moves down every column. Only the subject changes.</text>
</svg>
</div>

*Four jobs, one muscle: give direction, verify anyway, then fix the reasoning instead of the output.*

Some evenings I've got five to ten Claude sessions open at once, and it feels *exactly* like running a team:
delegate the work, keep each agent productive, and stitch what comes out into something that works.
When I read through what they produce, it's the same muscle I use reviewing a junior's pull request or grading a student's submission. I ask the same questions: "Does this meet the objectives?", "Do they understand what they shipped?", "Is it in line with our established standards and architecture?". My one rule for students turns out to be my one rule for the machine: if you can't explain it, you didn't build it.

> I'm not learning a new skill with AI. I'm spending the one I already paid for. The AI is just the newest, fastest, least-tenured member of a team I already knew how to run.

## The receipts

Everything below is pulled straight from the git history of the thing you're reading this on: my
portfolio, built on three tools I made for it. My own no-build framework (BATCH), my own design
system (GRAIN), and a little Markdown CMS that renders these very words into the page (MILL). 

Not a demo, not a tutorial repo. The thing I actually build on, and it's live now, so you're reading
this on it. Every count below was measured on 19 August 2026 from committed files only, because those
are the ones you can go and check. Counting my working tree, where the plans and the agent-facing
notes live deliberately uncommitted, gives me a friendlier number. I'm not using it.

<!-- REFRESH THESE NUMBERS (occasionally, and definitely before publishing) — the ratio + counts are a
     living git snapshot. Last measured 2026-08-22 (committed/tracked): 503 commits, 36,837 prose,
     34,595 code, 52% prose, 88,018 lines added. Previous marker, 2026-08-19: 440 commits, 31,228
     prose, 30,082 code, 51% prose, 76,497 added. Marker before that, 2026-08-07: 242 commits, 17,610
     prose, 18,876 code, 48% prose, 46,665 added, the pass where writing first passed code.
     Earliest marker, 2026-07-03: 33 commits, 6,947
     prose, 5,411 code, 56% prose, 13,395 added — kept because the DRIFT is now part of the argument
     in "The one number that matters". HOW TO RECOMPUTE, run from the repo root:
       commits:  git rev-list --count HEAD
       prose:    git ls-files '*.md' | xargs wc -l | tail -1
       code:     git ls-files '*.ts' '*.js' '*.css' '*.html' | xargs wc -l | tail -1
       prose %:  prose / (prose + code)   (6947 / (6947 + 5411) = 56.2%)
       added:    git log --numstat --pretty=tformat: | awk '{a+=$1} END{print a}'
     NOTES: "code" = ts+js+css+html, NOT TypeScript alone (TS-only is ~3,100 and would read ~69% prose,
     which is why the prose says "not code", not "not TypeScript"). Basis is COMMITTED/tracked files
     (the honest "git history" number); counting the working tree incl. uncommitted files runs higher
     (~59% prose). Scope today = the whole batch-stack monorepo; before publishing the portfolio decide
     whether to narrow to the portfolio app (batch+grain+mill + this site) or keep the full stack.
     Update in lockstep when you refresh: this SVG's bar values/labels + aria-label (bar widths are
     proportional: prose bar = 420px max, code bar = round(420 * code/prose)) and the "6,947 prose /
     5,411 code" pull-quote just above. The frontmatter subtitle/summary state the ratio WITHOUT exact
     counts on purpose (don't re-add numbers there). See CONTENT-BACKLOG.md. -->


## The one number that matters

Let me get the headline out of the way, because it reframes everything else:

> **More than half of this repo is writing, not code.** 36,837 lines of words against 34,595 lines
> of code. Ask a room to guess and they say twenty percent.

Sit with that for a second, because it's the opposite of what vibe coding is supposed to look
like. The caricature is a person who writes *no* docs, who couldn't produce a spec if you begged.
This repo has the inverse problem: there's about as much specification as there is implementation.

I didn't tidy that up after the fact. It's the method. When you work with an AI at speed, the scarce resource stops being code and becomes *intent.* The model can't know what you meant, what the rules are, or what "done" looks like, unless you wrote it down. So I do. The documentation *is* the work, the code is the cheap part the machine handles.

<div class="live-fig" data-live-figure="ratio" data-surface="figure:ratio">
<svg viewBox="0 0 620 200" width="100%" role="img"
     aria-label="On this repo: 34,595 lines of code versus 36,837 lines of writing. 52% of everything written is words, not code. A marker shows where the writing bar would end at the 20% a room typically guesses, well short of where it actually ends."
     style="max-width:560px;height:auto;font-family:Georgia,'Times New Roman',serif;--paper:#faf7f1;--edge:#e6ddd0;--ink:#2b2b2b;--muted:#6b6259;--bar:#cbc1b3;--accent:#d97757"
     xmlns="http://www.w3.org/2000/svg">
  <rect x="0.5" y="0.5" width="619" height="199" style="fill:var(--paper);stroke:var(--edge)"/>
  <text x="28" y="36" style="fill:var(--muted);font-size:15px">What my repo actually is</text>
  <text x="28" y="86" style="fill:var(--ink);font-size:14px">Code</text>
  <rect x="96" y="72" width="394" height="20" style="fill:var(--bar)"/>
  <text x="498" y="87" style="fill:var(--muted);font-size:12.5px">34,595 lines</text>
  <text x="28" y="128" style="fill:var(--ink);font-size:14px">Writing</text>
  <rect x="96" y="114" width="420" height="20" style="fill:var(--ink)"/>
  <text x="524" y="129" style="fill:var(--ink);font-size:12.5px">36,837 lines</text>
  <!-- where the writing bar would END if the room's usual guess were right. 20% of the repo is
       14,286 lines, which on this axis (420px = 36,837 lines) lands at x = 259. The writing bar is
       now the longer of the two, so it carries the full 420 and code is scaled against it. -->
  <line x1="259" y1="106" x2="259" y2="142" style="stroke:var(--muted);stroke-width:1;stroke-dasharray:3 3"/>
  <text x="267" y="152" style="fill:var(--muted);font-size:12px">where a room's guess of 20% would end</text>
  <text x="28" y="176" style="fill:var(--accent);font-size:13px">52% of everything written is words, not code.</text>
</svg>
</div>

*About as much spec as implementation: the inverse of what vibe coding is supposed to look like.*

One honest wrinkle, since I keep the receipts either way. When I first published this the same
count read 56%, and the writing was ahead. It has since evened out, for a boring reason: the stack
moved into its own repos and took a big slice of its own documentation with it, while the site's
code kept growing. Across all seven repositories I work in, one line in three is still
documentation. The ratio moved. The habit didn't.

## Now the "vibe" part, because I do go fast

<!-- LIVING NUMBERS (refresh occasionally, and definitely before publish): the counts below grow with
     every commit. Last measured 2026-08-22: 503 commits, 88,018 lines added, and the sprint reset to
     the night of 4 July 2026 measured ACROSS ALL SEVEN REPOS (21:08 to 03:31, 50 commits, 4,269 added)
     because the old 7pm-5am/31-commit window was the single-repo era. Previous marker 2026-07-03:
     33 commits, 31-commit sprint, 13,395 added. HOW TO RECOMPUTE, from the repo root:
       commits:  git rev-list --count HEAD
       added:    git log --numstat --pretty=tformat: | awk '{a+=$1} END{print a}'
     The sprint window (7pm-5am, 31 commits) comes from `git log --pretty='%h %ad %s' --date=format:'%H:%M'`
     over the sprint day — eyeball the timestamps. Keep in sync with the ratio SVG + frontmatter; the
     full recompute process (prose/code/ratio) lives in the REFRESH note under "The one number that matters". -->

If the docs number makes me sound slow and fussy, the tempo says otherwise. Here's the shape of the build:

- 503 commits on this repo alone, and every single one of them was co-authored with an AI. That's
  not the machine writing while I watched, I still write plenty of the code myself. Co-authored is
  the honest word: two sets of hands, on every commit.
- Across the seven repositories I keep, 1,246 commits between 27 June and 22 August. Eight weeks.
- One night in the middle of that, 4 July: **50 commits between 9pm and half past three, 4,269 lines
  added before it got light.** One every eight minutes. That's vibe coding at full tilt, the machine
  keeping up with the ideas as fast as I could aim them.
- Small commits, on purpose. Each one is a thing that already works, which is what makes the next
  one cheap to check. Fifty tiny commits you can verify beat five big ones you can't.
- 88,018 lines added to this repo since the first one. Real output.

Both things are true at once, and that's the entire point: fast *and* documented. The reason I can
sprint for ten hours and not wake up to a pile of nonsense isn't
that I'm careful *instead* of fast. It's that I did the careful part *first*: I built the rails
before I opened the throttle.

<div class="live-fig" data-live-figure="sprint" data-surface="figure:sprint">
<svg viewBox="0 0 620 160" width="100%" role="img"
     aria-label="One night, 4 July 2026: 50 commits from 9:08pm to 3:31am, about six and a half hours, roughly one commit every eight minutes, and 4,269 lines added. Every commit co-authored by a machine."
     style="max-width:560px;height:auto;font-family:Georgia,'Times New Roman',serif;--paper:#faf7f1;--edge:#e6ddd0;--ink:#2b2b2b;--muted:#6b6259;--bar:#cbc1b3;--accent:#d97757"
     xmlns="http://www.w3.org/2000/svg">
  <rect x="0.5" y="0.5" width="619" height="159" style="fill:var(--paper);stroke:var(--edge)"/>
  <text x="28" y="30" style="fill:var(--muted);font-size:15px">One night, 4 July 2026</text>
  <text x="310" y="60" text-anchor="middle" style="fill:var(--ink);font-size:14px">50 commits, about one every 8 minutes</text>
  <line x1="40" y1="86" x2="580" y2="86" style="stroke:var(--edge);stroke-width:2"/>
  <line x1="40" y1="78" x2="40" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="51" y1="78" x2="51" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="62" y1="78" x2="62" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="73" y1="78" x2="73" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="84" y1="78" x2="84" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="95" y1="78" x2="95" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="106" y1="78" x2="106" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="117" y1="78" x2="117" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="128" y1="78" x2="128" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="139" y1="78" x2="139" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="150" y1="78" x2="150" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="161" y1="78" x2="161" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="172" y1="78" x2="172" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="183" y1="78" x2="183" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="194" y1="78" x2="194" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="205" y1="78" x2="205" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="216" y1="78" x2="216" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="227" y1="78" x2="227" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="238" y1="78" x2="238" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="249" y1="78" x2="249" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="260" y1="78" x2="260" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="271" y1="78" x2="271" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="282" y1="78" x2="282" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="293" y1="78" x2="293" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="304" y1="78" x2="304" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="316" y1="78" x2="316" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="327" y1="78" x2="327" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="338" y1="78" x2="338" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="349" y1="78" x2="349" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="360" y1="78" x2="360" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="371" y1="78" x2="371" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="382" y1="78" x2="382" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="393" y1="78" x2="393" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="404" y1="78" x2="404" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="415" y1="78" x2="415" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="426" y1="78" x2="426" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="437" y1="78" x2="437" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="448" y1="78" x2="448" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="459" y1="78" x2="459" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="470" y1="78" x2="470" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="481" y1="78" x2="481" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="492" y1="78" x2="492" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="503" y1="78" x2="503" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="514" y1="78" x2="514" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="525" y1="78" x2="525" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="536" y1="78" x2="536" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="547" y1="78" x2="547" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="558" y1="78" x2="558" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="569" y1="78" x2="569" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <line x1="580" y1="78" x2="580" y2="94" style="stroke:var(--ink);stroke-width:1"/>
  <text x="40" y="118" style="fill:var(--muted);font-size:12.5px">9:08pm</text>
  <text x="310" y="118" text-anchor="middle" style="fill:var(--muted);font-size:12.5px">the small hours</text>
  <text x="580" y="118" text-anchor="end" style="fill:var(--muted);font-size:12.5px">3:31am</text>
  <text x="28" y="146" style="fill:var(--accent);font-size:13px">4,269 lines added before it got light. Every commit co-authored.</text>
</svg>
</div>

*Six and a half hours, fifty commits: fast is safe when the rails went up first.*

## The part that actually makes it professional

The distinction that matters has nothing to do with whether you use AI or how fast you
type. **Amateur vibe coding is unstructured improvisation. Professional vibe coding is structured
delegation.** Same tools, same speed, wildly different amounts of scaffolding around the model.

And the clearest way to see it is to split the job in two:

> The AI can generate a thousand lines an hour. It cannot decide the layering rule, the token
> architecture, the single write door, or what "done" means. *Those were mine.* The machine typed;
> I engineered.

The taste, the architecture, the judgment about what's worth building and what's a trap: that's the
human. I don't make the AI reliable by writing cleverer one-off prompts. I make it reliable by
building it an environment where good work is the path of least resistance.

That environment is a system, and it's copyable.

### The playbook

**1. Write the rails before the features.** Before I built much of anything, I wrote the rulebook.
The repo has a [conventions doc](/batch/docs/conventions), the build standard: layering rules, how
components are structured, the testing bar. And an [onboarding doc](https://github.com/tjakoen/bread/blob/main/CLAUDE.md) written *for the
AI*, so any model (or human) joining the project reads the same "here's how this works, here's what
not to touch" before it writes a line. The AI goes fast because the guardrails are already up. You
don't tell a fast driver to be careful; you build the track.

**2. Give the AI a memory.** The model forgets everything between sessions, so I keep a running set
of decision notes, twenty-odd small records of *why* things are the way they are. We went through
the single write door because of this. The theme is decoupled because of that. Next session the AI
inherits the reasoning instead of relitigating it, or worse, quietly undoing it. A professional
keeps a paper trail. So does my repo.

**3. Make change propagate: keep a sync map.** The failure mode of fast AI work isn't bad code, it's
*drift*: you change one thing and six related things silently fall out of step. So I keep an explicit
"when you change this, also update that" table in the onboarding doc. Touch an action verb? Here are
the five places that have to move with it. It turns "don't forget" into a checklist a machine can
actually follow.

**4. Audit relentlessly. Never assume the AI knew what it was doing.** You review a junior's pull
request for a reason, and a model earns *more* of that scrutiny, not less, because it's fluent enough
to sound right exactly when it's wrong.

> Most of my prompting isn't "build this." It's "check this."

Does it still match the goal? Does it honor the docs? Did it quietly break a rule to get something
working? That's where most of the hours actually go: not building, checking. Then before the big
commits there's a [repeatable runbook](https://github.com/tjakoen/bread/blob/main/AUDIT.md): tests green? Layers still importing in
one direction only? Any hardcoded values that should be design tokens? Going fast is only safe when
there's a cheap, repeatable way to prove you didn't bend the shape of the thing.

What that adds up to, counted the same day as everything else here: 184 test files across the seven
repositories, roughly 2,159 test cases behind them, and 44 end-to-end specs on this site alone, all
of it behind one command that has to come back green before anything counts as done. There's even a
linter for the prose, which sounds absurd until you remember that half of what's in here is prose.
If the writing is part of the product, the writing gets a gate.

One rule I'd hand a student before any of the tooling: the second pass has to be done by someone who
didn't write the code. A session that just wrote something already believes it, and will defend it
convincingly. A cold session won't. That isn't a clever trick, it's the same reason you don't mark
your own exam.

**5. Tests are part of the work, not a phase after it.** Three tiers (unit, integration,
end-to-end) written *as* features, not bolted on later. The type-checker and the test suite
have to be green before I call anything done. It's the least glamorous rule and it does the most
work: it's the seatbelt that lets me drive fast.

**6. Document for the machine, on purpose.** Underneath the other five is one mindset: the AI is a
first-class reader, so I write *for* it, not *around* it. It reads the same onboarding and conventions
a new teammate would. And I've started taking that down to the component level in my design system,
where the pieces the AI actually operates get a second doc written only for the machine: how to drive
the thing, not how a human reads about it. You onboard this collaborator every time you spin up a new session, so you build
the onboarding, make it excellent, and polish it as you go.

<div class="live-fig" data-live-figure="loop" data-surface="figure:loop">
<svg viewBox="0 0 252 340" width="100%" role="img"
     aria-label="The working loop: rails (conventions and onboarding), memory (decisions carry over), a sync map (change propagates), audit (check, don't trust), and tests (the seatbelt), then back to the work."
     style="display:block;width:100%;max-width:430px;height:auto;margin:0 auto 1.5rem;font-family:Georgia,'Times New Roman',serif;font-size:13.5px">
  <defs>
    <marker id="fl-tentim0" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" style="fill:var(--color-muted)"/>
    </marker>
  </defs>
  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="16" y="16" width="167" height="52" rx="6"/>
    <rect x="30" y="80" width="140" height="52" rx="6"/>
    <rect x="34" y="144" width="131" height="52" rx="6"/>
    <rect x="37" y="208" width="126" height="52" rx="6"/>
    <rect x="52" y="272" width="95" height="52" rx="6"/>
  </g>
  <g style="stroke:var(--color-muted);stroke-width:1.5;fill:none">
    <line x1="100" y1="68" x2="100" y2="80" marker-end="url(#fl-tentim0)"/>
    <line x1="100" y1="132" x2="100" y2="144" marker-end="url(#fl-tentim0)"/>
    <line x1="100" y1="196" x2="100" y2="208" marker-end="url(#fl-tentim0)"/>
    <line x1="100" y1="260" x2="100" y2="272" marker-end="url(#fl-tentim0)"/>
    <path d="M147,298 C213,298 213,42 183,42" marker-end="url(#fl-tentim0)"/>
  </g>
  <g text-anchor="middle">
    <text x="100" y="38.3" style="fill:var(--color-fg)">Rails</text>
    <text x="100" y="54.8" style="fill:var(--color-muted);font-size:12px">conventions + onboarding</text>
    <text x="100" y="102.3" style="fill:var(--color-fg)">Memory</text>
    <text x="100" y="118.8" style="fill:var(--color-muted);font-size:12px">decisions carry over</text>
    <text x="100" y="166.3" style="fill:var(--color-fg)">Sync map</text>
    <text x="100" y="182.8" style="fill:var(--color-muted);font-size:12px">change propagates</text>
    <text x="100" y="230.3" style="fill:var(--color-fg)">Audit</text>
    <text x="100" y="246.8" style="fill:var(--color-muted);font-size:12px">check, don't trust</text>
    <text x="100" y="294.3" style="fill:var(--color-fg)">Tests</text>
    <text x="100" y="310.8" style="fill:var(--color-muted);font-size:12px">the seatbelt</text>
  </g>
  <g text-anchor="middle" style="fill:var(--color-muted);font-size:12px;stroke:var(--color-bg);stroke-width:3;paint-order:stroke">
    <text x="222" y="170" transform="rotate(-90 222 170)">back to the work</text>
  </g>
</svg>
</div>

*The loop is what keeps fast from turning into fragile.*

### Two of those rules have proper names, and I didn't invent either

Worth saying plainly, because I read my way here and you can too.

**Context engineering** is the one everybody gets wrong first. Your prompt is not the input. The
model reads the room: the files it was pointed at, the conventions it found, whatever the last
session left behind. It arrives with no memory of yesterday and none of the things somebody told you
in person. So the instinct to fix a bad answer by writing a cleverer prompt is the amateur move, and
I made it for months before the penny dropped. I stopped prompting harder and started leaving better
notes.

There's a free test for this and it takes two minutes. Open a cold session, no chat history, give it
one sentence of instruction, and watch what it does first. If it guesses the stack, invents a folder,
or writes a framework into a project that has never had one, that gap is yours and not the model's.
Alfonso Graziano's [Learning AI-Native Software Engineering](https://alfonsograziano.it/book) is
where most of my thinking on this got sharpened, along with spec-driven work like
[GitHub's Spec Kit](https://github.com/github/spec-kit).

**Loop engineering** is the other one, and it's Addy Osmani's
[term and framing](https://addyosmani.com/blog/loop-engineering/): durable AI work is built out of a
few reusable parts rather than one clever prompt. His [Beyond Vibe Coding](https://beyond.addy.ie)
carries the harder half of it, the seventy percent problem, where the machine gets you most of the
way and the last stretch is exactly where unmanaged work rots.

The shape I run per change is four moves. Brief it, which is what to build and what not to touch.
Build, where the machine types and I read every line. **Verify**, and that word is doing all the
work here. Then record what changed, where the next session will find it.

Verify is not a feeling. It's the type checker, the tests, an end-to-end run against the real thing,
and a pair of eyes that isn't mine. Skip it once and nothing happens, which is the trap. Skip it for
a week and you can't answer a single question about your own repository, and neither can the machine,
because nothing you agreed five sessions ago survived anywhere it could read.

## The best trick of all: the AI's mistakes are a measurement

Here's the move I'm proudest of. While I was building one
of GRAIN's showcase pages, I kept making small mistakes: the grain texture not showing on the AI's
text, a "chat" that rendered as two stacked boxes instead of a conversation, an AI "Send" click that
didn't actually look like a click. Normal bug-fixing territory. The boring move is to squash each one
and move on.

Instead we stopped, the machine and I, and started asking questions: why do I keep tripping on my
*own* system? Am I the problem? Is the architecture I planned out the problem? I am designing my framework in a way that's supposed to be easy for a human to use. Much less an AI. So why is it tripping up on trivial things?

It's the same reflex I use giving feedback on a code review or grading a student's project: when the same mistake keeps coming back, you stop blaming the person making it and start suspecting the thing
they're using. When half a class flubs the same question, the problem is the question, not the class.

> If the person who designed it keeps slipping, the architecture isn't clear, and it's meant to be easy for a human, never mind an AI.

So the mistakes stopped being bugs and started being data.

We ran a proper review, and the mistakes weren't random. Every one clustered in a single failure mode:
silent-failure contracts. Mechanisms that quietly do nothing when you use them slightly wrong: no
error, no warning, the documented way just doesn't take. That's the most dangerous kind of design
there is, especially for a system whose whole pitch is "legible to a human and an AI." A thing that
fails loudly, you fix. A thing that fails silently, you ship.

The AI surfaced it in a way I couldn't have alone, because an AI working from your docs is the
ultimate stress-test of clarity. It has no tribal knowledge, no "oh, everyone knows you do it this
way." So when it trips on your system, that isn't the machine being dumb; it's a free audit telling
you a tired human on a Friday will trip on the exact same spot. Read the signal instead of just
correcting the output.

So we didn't document around the traps or bolt on a test to catch them later. We designed them out:
made the grade work on any element, shipped the chat as a container so messages *can't* misalign.
"Can't be done wrong" beats "we'll catch it if it is." Then we backstopped it with tests that assert
the design system is *used correctly*, not merely that the page renders, including one that proves a
human action and an AI action produce the exact same result. Those tests are executable contracts:
the next person, human or AI, gets caught the moment they misuse the thing, long after I've left the
conversation. And every fix got proven against reality: computed styles, geometry, screenshots, real
end-to-end runs, never "it probably looks right."

The one-sentence version: *where the AI keeps slipping* is a map of where my design isn't clear yet,
and I harden the design so neither of us slips there again.

<div class="live-fig" data-live-figure="trap" data-surface="figure:trap">
<svg viewBox="0 0 231 340" width="100%" role="img"
     aria-label="Turning a mistake into a guardrail: the AI trips on a documented idiom; read it as a clarity signal, not a bug; find the silent-failure contract; design the misuse out; lock it with a test at human-equals-AI parity; and it becomes a permanent guardrail."
     style="display:block;width:100%;max-width:430px;height:auto;margin:0 auto 1.5rem;font-family:Georgia,'Times New Roman',serif;font-size:13.5px">
  <defs>
    <marker id="fl-tentim1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" style="fill:var(--color-muted)"/>
    </marker>
  </defs>
  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="18" y="16" width="143" height="52" rx="6"/>
    <rect x="16" y="80" width="147" height="52" rx="6"/>
    <rect x="27" y="144" width="125" height="52" rx="6"/>
    <rect x="40" y="208" width="99" height="52" rx="6"/>
    <rect x="17" y="272" width="144" height="52" rx="6"/>
  </g>
  <g style="stroke:var(--color-muted);stroke-width:1.5;fill:none">
    <line x1="89" y1="68" x2="89" y2="80" marker-end="url(#fl-tentim1)"/>
    <line x1="89" y1="132" x2="89" y2="144" marker-end="url(#fl-tentim1)"/>
    <line x1="89" y1="196" x2="89" y2="208" marker-end="url(#fl-tentim1)"/>
    <line x1="89" y1="260" x2="89" y2="272" marker-end="url(#fl-tentim1)"/>
    <path d="M162,298 C192,298 192,42 161,42" marker-end="url(#fl-tentim1)"/>
  </g>
  <g text-anchor="middle">
    <text x="89" y="38.3" style="fill:var(--color-fg)">AI trips on a</text>
    <text x="89" y="54.8" style="fill:var(--color-fg)">documented idiom</text>
    <text x="89" y="102.3" style="fill:var(--color-fg)">Read it as a clarity</text>
    <text x="89" y="118.8" style="fill:var(--color-fg)">signal, not a bug</text>
    <text x="89" y="166.3" style="fill:var(--color-fg)">Find the silent-</text>
    <text x="89" y="182.8" style="fill:var(--color-fg)">failure contract</text>
    <text x="89" y="230.3" style="fill:var(--color-fg)">Design the</text>
    <text x="89" y="246.8" style="fill:var(--color-fg)">misuse out</text>
    <text x="89" y="294.3" style="fill:var(--color-fg)">Lock it with a test</text>
    <text x="89" y="310.8" style="fill:var(--color-fg)">human = AI parity</text>
  </g>
  <g text-anchor="middle" style="fill:var(--color-muted);font-size:12px;stroke:var(--color-bg);stroke-width:3;paint-order:stroke">
    <text x="201" y="170" transform="rotate(-90 201 170)">becomes a permanent guardrail</text>
  </g>
</svg>
</div>

*Every trip the AI takes is a map of where the design isn't clear yet, and a chance to harden it.*

## The tell is the ratio

Strip all of it down and you land right back at that first number. **52% writing.** About as much
spec as implementation, in a repo whose whole reputation is supposed to be the opposite.

An amateur and I could type the exact same request into the exact same model and get back the exact
same code. The difference isn't in that moment. It's in everything I built *around* the moment: the
conventions the model read first, the memory it inherited, the tests it has to pass, the audit it has
to survive. That surrounding structure is the profession. The code is just the code.

And it's the same law as the napkin at the top: the model multiplied what I brought. I brought
conventions, memory, tests, and taste, so it multiplied *those*.

<svg viewBox="0 0 620 300" width="100%" role="img"
     aria-label="Two identical chains of prompt, model and code. The first stands bare, and that is the whole system. The second, identical in the middle, sits inside a built loop: a brief, the conventions and the inherited memory feed the prompt; the code then has to survive the types, the tests, an end-to-end run and my own review; and what changed is recorded back into the brief, so the next session starts there instead of from nothing."
     style="display:block;width:100%;max-width:560px;height:auto;margin:0 auto 1.5rem;
            font-family:Georgia,'Times New Roman',serif;font-size:12.5px">
  <defs>
    <marker id="fl-tentim2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" style="fill:var(--color-muted)"/>
    </marker>
  </defs>

  <text x="16" y="40" style="fill:var(--color-muted)">anyone</text>
  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="96" y="16" width="76" height="32" rx="5"/>
    <rect x="192" y="16" width="76" height="32" rx="5"/>
    <rect x="288" y="16" width="76" height="32" rx="5"/>
  </g>
  <g style="stroke:var(--color-muted);stroke-width:1.5;fill:none">
    <line x1="172" y1="32" x2="187" y2="32" marker-end="url(#fl-tentim2)"/>
    <line x1="268" y1="32" x2="283" y2="32" marker-end="url(#fl-tentim2)"/>
  </g>
  <g text-anchor="middle" style="fill:var(--color-fg)">
    <text x="134" y="36">prompt</text><text x="230" y="36">model</text><text x="326" y="36">code</text>
  </g>
  <text x="384" y="36" style="fill:var(--color-muted)">and that is the whole system</text>

  <text x="16" y="180" style="fill:var(--color-muted)">me</text>
  <rect x="80" y="72" width="524" height="222" rx="8" style="fill:none;stroke:var(--color-line);stroke-width:1"/>

  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="100" y="90" width="146" height="26" rx="4"/>
    <rect x="262" y="90" width="146" height="26" rx="4"/>
    <rect x="424" y="90" width="146" height="26" rx="4"/>
  </g>
  <g text-anchor="middle" style="fill:var(--color-fg);font-size:11px">
    <text x="173" y="107">the brief it reads first</text>
    <text x="335" y="107">the conventions</text>
    <text x="497" y="107">the memory it inherited</text>
  </g>
  <g style="stroke:var(--color-muted);stroke-width:1.2;fill:none">
    <path d="M173 116 L173 130 M335 116 L335 130 M497 116 L497 130"/>
    <path d="M173 130 L497 130"/>
    <path d="M335 130 L335 146" marker-end="url(#fl-tentim2)"/>
  </g>

  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="196" y="150" width="76" height="32" rx="5"/>
    <rect x="292" y="150" width="76" height="32" rx="5"/>
    <rect x="388" y="150" width="76" height="32" rx="5"/>
  </g>
  <g style="stroke:var(--color-muted);stroke-width:1.5;fill:none">
    <line x1="272" y1="166" x2="287" y2="166" marker-end="url(#fl-tentim2)"/>
    <line x1="368" y1="166" x2="383" y2="166" marker-end="url(#fl-tentim2)"/>
  </g>
  <g text-anchor="middle" style="fill:var(--color-fg)">
    <text x="234" y="170">prompt</text><text x="330" y="170">model</text><text x="426" y="170">code</text>
  </g>

  <g style="stroke:var(--color-muted);stroke-width:1.2;fill:none">
    <path d="M426 182 L426 198 M118 198 L570 198"/>
    <path d="M154 198 L154 214 M268 198 L268 214 M382 198 L382 214 M496 198 L496 214"/>
  </g>
  <g style="fill:none;stroke:var(--color-line);stroke-width:1">
    <rect x="100" y="216" width="108" height="26" rx="4"/>
    <rect x="214" y="216" width="108" height="26" rx="4"/>
    <rect x="328" y="216" width="108" height="26" rx="4"/>
    <rect x="442" y="216" width="108" height="26" rx="4"/>
  </g>
  <g text-anchor="middle" style="fill:var(--color-fg);font-size:11px">
    <text x="154" y="233">the types</text>
    <text x="268" y="233">the tests</text>
    <text x="382" y="233">end to end</text>
    <text x="496" y="233">my own review</text>
  </g>

  <g style="stroke:var(--color-muted);stroke-width:1.2;fill:none">
    <path d="M550 229 L578 229 L578 274 L92 274 L92 103 L94 103" marker-end="url(#fl-tentim2)"/>
  </g>
  <g text-anchor="middle" style="fill:var(--color-muted);font-size:11px;
       stroke:var(--color-bg);stroke-width:3;paint-order:stroke">
    <text x="330" y="262">record what changed, so the next session starts up there</text>
  </g>
</svg>

*Nothing in the middle row is better. The whole difference is what got built around it.* [Play with this one](/talks/ten-times-zero#27).

## Don't work for the AI. Run it.

If you take one idea out of all of this, take this one, because it decides which of the two
developers you become.

Working *for* it looks like this. You ask, it answers, you paste. The shape of the project ends up
being whatever the machine happened to produce that afternoon. When something goes wrong you ask it
again, louder. And you spend a lot of your day sitting in front of one chat window, waiting.

Running it looks different from the first minute, before a single prompt gets typed. You decide what
should exist. You cut it into pieces small enough to check in a minute each. You point the machine
at one piece and read every line that comes back. And you accept nothing you couldn't defend with
the tab closed.

Same tool, same day, same model. One of those is a job. The other is a habit that does not survive
contact with anything real. Some evenings I have several sessions open at once and it feels exactly
like running a team, because it is the same skill: brief, delegate, verify, and never sign off on
work you can't explain.

## Where to start, if you're early

Concrete, and in this order, because everybody wants to start at three.

**1. Pick a project you already understand.** Not a new one. You can't judge the output of a machine
on a codebase you've never read, and judging the output is the entire skill.

**2. Write the brief before the feature.** One file at the root: what this is, how it's built, what
not to touch. Twenty minutes, once. Every session after it starts smarter, and you'll feel it on day
two.

**3. Turn on the cheap gates first.** Types on and strict, one test that actually runs, no empty
catch blocks. All three before lunch, and they'll keep catching things all year.

**4. Make it explain, then explain it back.** Out loud, tab closed, to nobody. The moment you can't,
stop. You've just found the exact thing to go and learn, and it's worth more than the feature was.

**5. Every correction you give twice becomes a line in that file.** That's how the brief writes
itself, and it's the whole method minus my tooling. The second time you correct a machine about the
same thing, that correction has stopped being a conversation and started being a rule.

And then the part most people skip: record it. In your docs, in your audit, in the file the model
reads first. Let it learn the same lesson you just did, so tomorrow you get to hold it to that
lesson instead of teaching it again.

If you're early in your career and using AI to produce, you're buying output. If you're using it to
learn, you're buying reps. Same hour, and only one of those makes you a bigger number.

## The point

So yes: I'm a vibe coder. I let the machine write nearly everything, I work in fast overnight bursts,
and I'll tell anyone who asks that this is the most productive I've ever been. But I can also hand you
the receipt (the commits, the ratio, the runbook) and show you it wasn't luck.

That's the whole word. *Professional.* It just means I keep the receipts. Get so good at the
fundamentals that when you finally pick up the power tool, it multiplies something real. It's the
same discipline I drill into my students when I teach, and the same rule I turn on my own AI when it grades their work: [the console I built to stop drowning](the-console-i-built-to-stop-drowning.md).

Bring nothing and it multiplies nothing: ten times zero is still zero. So become a bigger number
first, then go make it dangerous.

> I don't prompt and pray. I prompt and prove.

## What I read to get here

None of the ideas underneath this are mine. I read these, argued with them, and built something
that runs, which is a method you can copy today.

- [Loop Engineering](https://addyosmani.com/blog/loop-engineering/), Addy Osmani. The case that
  durable AI work is built from a few reusable parts rather than a clever prompt, plus the warning
  about comprehension debt that I wish I'd read two years earlier.
- [Beyond Vibe Coding](https://beyond.addy.ie), Addy Osmani. The seventy percent problem, plan
  first over prompt and pray, and quality gates as non-negotiable. This one is why my session
  heartbeat and my gates exist at all.
- [Learning AI-Native Software Engineering](https://alfonsograziano.it/book), Alfonso Graziano.
  Context engineering, spec-driven development, verification gates.
- [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills). The loop in executable
  form, as skills an agent loads on its own. Worth reading even if you never run them.
- [GitHub Spec Kit](https://github.com/github/spec-kit). Spec-driven development, formalized and
  free. The most direct way to try "write the rails first" without inventing your own process.
- [llms.txt](https://llmstxt.org) and [MCP](https://modelcontextprotocol.io). How a project tells an
  agent what to read first, and how an agent reaches your tools. An evening each.
- My own [standards](/standards), which are the written version of everything above as I actually
  run it. Take what's useful and ignore the rest.

---

*The judgment is human. The typing, by design, is not. On this one, nearly all of it.*
