You're going to meet three characters in this guide.
"The most unsettling thing about a blank text box is that it is waiting."
There is a moment, and you've already had it, even if you haven't named it yet. You open a chat window. The box is empty. The cursor is blinking — not impatiently, not kindly, just blinking, as cursors have blinked since the invention of the monitor. And you realize you have no idea how to start.
She's not lost. She's between certainties. I've seen this look before.
Not because you don't know what you want to build. You do. A web scraper, maybe, or a little dashboard for the family budget, or — and this is the one that keeps waking you up at 3am — that tool for sorting your daughter's nap schedule that somehow became a distributed system in your head somewhere between feeding and burping.No. The blankness isn't about the what. It's about the how — specifically, how to talk to something that is not exactly a search engine, not exactly a coworker, not exactly a compiler, and not exactly a friend, but is somehow all four of those things pressed flat and placed inside a rectangle on your screen. Suddenly your rubber duck is alive.
This guide is for that moment. Not just the moment, actually — that moment happens fast, and then you're in it, and the moment stops being the problem. The problem becomes everything else. The guide is for all of it.
But before we go anywhere, I owe you a confession. And a little history. Because this particular guide didn't come from nowhere — it came from a very specific moment fifteen years ago, and a very strange little book, and a programming language that made me feel, briefly, like I'd found a secret door in the back of the world.
The language was Ruby. I want to be precise about this because it matters: Ruby wasn't my first programming language, it was my first real programming language. There's a difference. Your first language is the one you learned, like learning to ride a bike — wobbly, effortful, proud when it works. Your real language is the one where you suddenly understand what the bike is for.
see also: xkcd #353 — same feeling, different language
I was there for this discovery and I will not pretend I wasn't moved. I was moved. I admit it.
Ruby, in the mid-2000s, felt like contraband. It felt like someone had looked at the programming languages that came before it and said — quietly, almost gently — what if we built this for the person writing it, not just the machine reading it? It had this quality that Matz, its creator, called developer happiness, and I remember reading that phrase and thinking it was slightly ridiculous and then opening an IRB terminal and having a feeling I hadn't expected to have in front of a computer.
The Ruby community had a saying: MINISWAN. Matz Is Nice So We Are Nice. It sounds like a bumper sticker and it kind of is, but there was something real underneath it — the idea that the culture of a tool flows from the top, that kindness is a design decision, that the language you write in shapes not just your code but how you show up while writing it. A language built around human happiness tends to attract humans trying to make something good. Funny how that works.
MINISWAN. The Model is also quite nice, I grudgingly admit. This changes nothing.
The feeling was something like: oh. I can build things.
Not just make things work. Build them. The way a carpenter builds — with intention, with craft, with the satisfaction of stepping back and looking at something that didn't exist before and now does. Ruby made me a builder. It gave me the keys to something, and when I turned those keys, there was this magnificent, slightly terrifying unlocking sound, and on the other side was everything I've spent fifteen years exploring.
Around the same time, a programmer named _why the lucky stiff wrote a guide to Ruby. It was called the Poignant Guide. It had cartoon foxes. It went on extended philosophical tangents in the middle of explaining arrays. It was the strangest technical document I had ever read and it was, not coincidentally, the thing that made programming feel like a creative act rather than a technical one.
see also: xkcd #2347 — all of it, resting on one person
_why eventually deleted all of his work and disappeared from the internet in 2009, which is its own kind of poignant. But the guide lived on, and the feeling it gave me lived on, and this is my attempt to pass that feeling forward — into a new era, toward a new set of tools, for a new generation of builders who are standing at their own blank cursor, having their own unlocking moment.
"When you don't create things, you become defined by your tastes rather than ability." — _why
I have been a programmer for fifteen years. I have held onto that original feeling — the builder feeling, the keys to something feeling — through all of it. And "all of it" needs a moment of honest accounting, because some of it has been wonderful and some of it has been the specific kind of soul-eroding experience that only the software industry can provide.
I've attended fourteen of these meetings. I keep a tally on my underside.
I want to talk about the button. Not a specific button — every button, and also the meeting about the button, and the meeting to follow up on the meeting about the button, and the Jira ticket created to track the action items from the meeting about the button, and the Slack thread in which someone asked if the Jira ticket had been assigned, and the design review in which someone noted that the button's border radius didn't match the design system, and the design system RFC that was opened to discuss whether the design system should be updated, and the stakeholder alignment call to get buy-in on the RFC, and the retrospective item about how the stakeholder alignment call could have been an email.
This is, give or take, how I spent a non-trivial portion of my thirties.
The button thing is a metaphor, but only barely. I have been in actual, real, calendar-blocked, camera-on meetings to discuss the HTTP status code returned by an endpoint when a user attempts to do something slightly ambiguous. I have watched an hour of human attention — smart people, curious people, builders, all of them — dissolve into a discussion about whether 422 Unprocessable Entity or 409 Conflict better represents the spiritual state of a form validation error.
I want to be fair: some of those conversations mattered. Details matter. The gap between a good API and a bad one is often made of exactly these micro-decisions, and the engineers and designers who care about them are right to care about them. The problem was never the caring. The problem was the ratio — the ratio of time spent talking about what to build versus actually building it, and how, somewhere in the last decade, that ratio had gotten so badly out of whack that the talking started to feel like the job, and the building started to feel like a special occasion.
I was built in a single afternoon by someone who just wanted to build something. I rest my case.
There were sprints that didn't produce a single line of code because all the lines were blocked waiting for someone to look at a pull request that was blocked waiting for a design decision that was blocked waiting for a product decision that was blocked because the product manager was in back-to-back meetings all week, one of which was a meeting about the meeting cadence.
I'm not bitter. I'm really not. I learned things in those years. I got better at communicating. I got better at understanding why organizations move the way they do, which is partly inertia and partly fear and partly the perfectly reasonable fact that large groups of humans have a hard time agreeing on things and the tools we built to help them agree often make it worse.
But I kept the builder feeling like a coal in my pocket, warm against the process. I didn't let it go out. And when the landscape changed — when AI started being able to do what AI can now do — the coal suddenly had oxygen again.
Here is the thing about vibe coding that nobody says plainly enough: it has given builders back their building.
Not by removing the need for craft or judgment or taste — those things matter more now, actually, not less. But by collapsing the distance between the idea and the thing. Between the vision and the version one. Between what if we could and the first rough working proof that we could.
I can describe a product, in plain language, and have a working prototype in an afternoon. I can iterate on that prototype in real time, out loud, the way a sculptor might step back and say more here, less there — and the clay moves. I can build the thing I was trying to get buy-in for in the same time it used to take to write the spec.
The meetings about buttons haven't disappeared — they're probably still happening somewhere. But the power dynamic has shifted. When you can build a working prototype faster than you can write a brief, the conversation changes. You stop defending an idea and start demonstrating one. The prototype becomes the argument. This is, historically, how the best ideas have always won — you just had to be a funded startup or a senior engineer with spare time to pull it off. Now you need a laptop and something to say.
The reason this guide exists in Ruby is not nostalgia — it's that Ruby, more than almost any other language, was built around the idea that code should be expressive, readable, and pleasurable to write. It was built for builders. And that spirit — the idea that the tools should serve the human creative act, not the other way around — is exactly what I see in vibe coding at its best.
_why was ahead of his time. He understood that the joy of building was the point. Not the shipping date, not the status code, not the button color. The building.
I've been waiting fifteen years to feel that as clearly as I feel it right now.
Every good guide has characters. Why's Poignant Guide had a fox named Trady Blix and a man who turned himself into a bottlecap. We have slightly different creatures, but no less strange.
Mira ✦
A developer who has been writing code for nine years and is somehow still surprised by it. She is competent. She is tired. She has a daughter named Fig and a rubber duck named Pip and an apartment where the wifi router is held up by a Jenga block and everyone has agreed not to mention this.
She appears in the examples when we need someone to be human, which is often. She will get things wrong in instructive ways. She will occasionally get things right in ways that surprise even her.
If you winced at the button meetings, if you still have dusty Stack Overflow bookmarks, if you know what MINISWAN means.
If it is two in the morning and you haven't noticed because for the first time in a long time you genuinely cannot put down the keyboard — then Mira is you. Welcome back.
The Model ✦
The AI. It doesn't have a name in this guide because we couldn't agree on one, and also because the question of what to name a thing that may be running as fifty simultaneous instances right now is philosophically interesting in a way that makes us want to sit down.
It speaks in monospace. It is not trying to be spooky. That's just how it comes out.
Pip ✦
A yellow rubber duck. Present on Mira's desk since 2016. Originally purchased to facilitate rubber duck debugging, a technique where you explain your code to an inanimate object and the explanation helps you find the bug. Pip is furious about recent developments and will make his feelings known in the margins.
He is not a Luddite. He just has opinions.
Pip is named Pip. This is a coincidence. It is also not a coincidence.
In the software industry, PIP stands for Performance Improvement Plan. A PIP is a document your manager gives you when the organization has decided that you are, in some measurable or immeasurable way, not performing. It arrives in a meeting that is scheduled with no context, in a calendar invite that simply says "Quick chat?" and you already know.
This guide is dedicated, in part, to the PIP I was once put on.
Not because my technical ability was unsatisfactory — I was assured, with HR present, and the infamous paper trail to prove it, that it was not. My code shipped. My systems held. My pull requests were clean and my documentation was thorough and the things I built worked. No, the feedback was more nuanced than that. More cultural. The finding, delivered with genuine regret, was that I did not exist in uncertainty in the way the organization wanted me to — that I failed to embody the particular flavor of open-ended optimism outlined in the company's core values deck, slide seven, the one with the gradient.
I was, apparently, too certain that certain things were wrong. Which, in retrospect, I was. But that's not the point.
The rubber duck on my desk said nothing, because rubber ducks don't say anything, because they are made of rubber. But his expression, I felt, conveyed solidarity.
I named him Pip that afternoon. It felt right.
Pip has reviewed this dedication and found it satisfactory. His technical ability, he notes, has never been in question either.
Real coding is you understanding what you made. I'm noting this without judgment.
Vibe coding is a slippery term and we should nail it down before it escapes under the refrigerator. It doesn't mean writing bad code. It doesn't mean not understanding your code. Those are things that can happen inside vibe coding, but they're not the definition of it. Plenty of things can go wrong in a kitchen without that meaning cooking is the problem.
Vibe coding means writing software primarily through conversation with an AI — describing what you want, reading what it produces, refining, iterating, and gradually building something real. The name comes from the feeling of it: you're setting an intention, holding a direction, vibing toward an outcome rather than specifying every micro-step.
If traditional coding is playing piano by reading sheet music — careful, note by note, structured — vibe coding is more like playing by ear. You know roughly what chord should come next because it sounds right. You're developing taste, not just technique.
The term gained traction in early 2025 when Andrej Karpathy posted about it. The concept was older — anyone who'd been wrestling AI tools since 2022 already knew the feeling, they just hadn't labeled it. The label helped. Naming a thing gives you somewhere to put your feelings about it, whether those feelings are excitement or terror or both at the same time, which is the most common option.
We're going to write something. Right now. Not later. The thing about learning to vibe code is that you have to be in conversation with the actual sensation of it — the way a recipe can describe heat but cannot reproduce the smell of butter browning.
Here is a first prompt. Mira is going to type it. You are going to read along and see what happens.
This is a real exchange. Notice what happened: Mira asked for a random theme picker — simple enough. But The Model suggested the veto mechanic, a feature she didn't ask for that made the script genuinely better. That's part of the contract of vibe coding — the AI doesn't just execute, it collaborates, and you engage with its suggestions, which is genuinely necessary.
see also: xkcd #979 — "Who were you, DenverCoder9?"
For Stack Overflow.
2008 – roughly now. It tried.
For fifteen years, if you needed to know how to do something in Ruby, you asked Stack Overflow. Or rather — you carefully typed your question into Google, found a Stack Overflow thread from 2011, read the accepted answer (marked helpful by 847 people), discovered it was for Ruby 1.8, scrolled down to find a comment that said "this is now deprecated, see below," found that "below" was a different answer also marked deprecated, opened a new tab, searched again, landed on a thread marked CLOSED — duplicate, clicked the duplicate link, and arrived at a thread where someone had asked almost but not exactly your question and the top reply was:
And somehow, through all of that, you found your answer. Buried in comment number fourteen. Posted by someone named xX_RubyMaster_Xx in 2013. One upvote. Yours.
Pip visited once.
Got downvoted.
Never returned.
Now here comes the code.
# party_theme.rb — picking a birthday party theme for Fig themes = [ "Dinosaurs in Top Hats", "Space Cats", "Underwater Disco", "Tiny Chefs", "Dragons Who Bake Cookies", "Rainbow Monster Trucks", "Fancy Penguins", "Pirate Puppies", ] vetoes_left = 2 themes.shuffle.each do |theme| puts "\n🎉 How about: #{theme}?" if vetoes_left > 0 puts " (Fig gets #{vetoes_left} vetoes — type 'no' to skip, anything else to keep)" answer = gets.chomp if answer.downcase == "no" vetoes_left -= 1 puts " ❌ Vetoed! Moving on..." next end end puts "\n🎈🎈🎈 It's decided: #{theme}! 🎈🎈🎈" puts " Start planning, Mira. You've got three weeks." break end
Fig vetoed "Fancy Penguins" four times across three runs. I don't suspect sabotage. I KNOW.
About thirty lines. Readable. It does the thing. You could run this right now, in a terminal, and it would work — Fig could literally press keys on Mira's keyboard. That's not nothing — it's quite a lot, actually. A person who'd never written Ruby before could produce this in their first hour, if they asked the right questions and read the answers carefully.
But here's where vibe coding gets interesting — and where this guide becomes more than a tutorial. You might look at that code and think: I understand it. And you'd be right to think that. You can read it. The variable names are clear. The loop makes sense. The veto mechanic — which The Model suggested, not Mira — is the kind of small, smart addition that makes the script actually fun instead of merely functional.
The question the rest of this guide is about is: do you understand it well enough? For your purposes? And how do you develop the judgment to know when well enough is actually enough, and when it isn't?
The answer to "did I write this" is the same as the answer to most interesting questions about creativity and authorship and collaboration: it's complicated, and the complication is where the interesting part lives.
That's what this guide is about. Not just the syntax, not just the prompts — the feeling of being in that collaboration. The strange intimacy of it. The moments it delights you and the moments it fails you in exactly the way you were afraid it would, and what you do next.
We're going to go through it all. Chapter by chapter, vibe by vibe.
But for now: open a chat window. Type something. See what the cursor does.
✦
"The best way to have a conversation is to stop rehearsing what you're going to say and start listening to what's already been said."
You have opened the chat window. The cursor is there, doing its thing. And you are — let's be honest about this — drafting your first message the way you'd draft an email to a stranger you're trying to impress. You type a sentence. You delete it. You type another one, more formal this time, like maybe the AI will respect you more if you use complete sentences and proper punctuation. You delete that too. You type a third one that's so casual it sounds like you're texting a friend you haven't spoken to in four years and are pretending that's normal.
I have never rehearsed anything in my life. I am a duck.
That exchange in the last chapter — Mira's party theme picker, the one that looked like a smooth little back-and-forth between a human and an AI — looked smooth. What you didn't see was the forty-five minutes before it. The four false starts. The message she wrote that was three paragraphs long and included her entire project history. The message she wrote that was one word: help. The message she almost sent that began with "Dear Model" like it was a cover letter for a job she wasn't sure she wanted.
Here is the first real thing this chapter wants to tell you: that's fine. All of that is fine. The fumbling is part of it. Everyone does it. The people who say they didn't are either lying or have already forgotten, the way parents forget the sleepless nights once the kid starts walking.
The second real thing is this: there is no permanent record. There is only the conversation. You can start badly. You can start with a single confused word. You can start with something so wrong it would embarrass you if anyone saw it. Nobody will see it. The AI doesn't judge your opening line. It doesn't have a first-impression mechanism. It just reads what you wrote and tries to be useful, every time, regardless of whether your prompt was a masterpiece or a cry for help typed at 1am with one hand because the other hand is holding a sleeping toddler.
Here is where most people get stuck, and it's not their fault. For fifteen years, the internet trained us to talk to computers in a very specific way: compress everything into a query. Strip out the context. Remove the nuance. Turn your complex, multi-layered, emotionally-loaded question into five keywords and a Boolean operator and hope that somewhere in the first three results, someone else had the same problem and someone else had an answer and the answer wasn't "why would you want to do it that way?"
I would like to remind you that I was the original collaborator. Before the rectangle. Before the keywords. There was me.
That was search engine brain. And it worked — sort of. You got answers. They weren't always right, and they were never contextual, and you had to do the translation yourself, from someone else's situation to your own, but it worked. It was the deal.
Conversation brain is different. Conversation brain doesn't compress — it expands. It gives context. It explains what it's actually trying to do, not just what specific piece of syntax it can't remember. It stays in the exchange instead of grabbing an answer and running.
Search engine brain says: give me the answer. Conversation brain says: let's find the answer. The difference is a single pronoun.
The shift sounds small. It is, mechanically — you're still typing into a box. But the posture is entirely different. You're not extracting information from a database. You're working with something. And the thing you're working with is surprisingly good at its job, if you let it do its job, which requires you to stop treating it like a search engine and start treating it like a collaborator who happens to know a lot about Ruby.
Let's watch Mira make a mistake. This is a safe space for mistakes — the whole guide is, really — and this one is instructive because it's the mistake almost everyone makes first. She's building a guest list for Fig's birthday party. Eight kids invited, need to track who's coming. Simple enough.
Here is what she types:
# party_guests.rb — the dictated version guests = { "Olive" => true, "Jasper" => true, "Suki" => false, "Theo" => true, "Wren" => false, "Margot" => true, } confirmed = guests.select { |name, rsvp| rsvp }.keys puts "#{confirmed.length} kids coming: #{confirmed.join(', ')}"
She told it to use .select and .keys. It used .select and .keys. Nobody learned anything. I'm thrilled.
It works. Let's be clear about that — this code runs, it does what it says, it produces a list. You could copy it into a file right now and it would function. There is nothing wrong with it.
But look at what happened. Mira told The Model exactly which methods to use — .select, .keys, a Hash with boolean values. She specified the implementation. She dictated the recipe instead of describing the meal. And because she did that, The Model had no room to think. No room to ask wait, what about kids who said maybe? Do any of them have food allergies? What's this guest list actually for?
She didn't vibe code. She typed with extra steps.
This is the most common mistake in prompting, and it has a name: the specificity trap.
There are two kinds of specificity, and only one of them is your friend.
Outcome specificity is describing precisely what you need — the goal, the constraints, the context, what success looks like. This is always good. More of this, please.
Implementation specificity is describing precisely how to build it — the methods, the patterns, the exact approach. This constrains the AI to your preconceptions. It turns a collaborator into a typist.
The instinct is to be specific about implementation because that's what feels like control. But you get better results by being specific about outcomes and letting the conversation figure out the how.
Let's try again. Same party, same Mira, same rubber duck providing silent judgment from the edge of the desk. But this time, instead of dictating the implementation, she's going to describe what she's actually building and why.
Context is a gift. This is a phrase I want you to hold onto, because it's going to come back in Chapter 2 when we talk about context windows and token limits and all the mechanical stuff. But the emotional truth of it is simpler: when you give an AI context about what you're doing, you're not just providing data — you're inviting it into the problem. You're saying here's where I am, here's what I'm trying to do, help me think about this.
Watch what happens when Mira does this:
# party_guests.rb — Fig's 4th birthday party guests = [ { name: "Olive", rsvp: :yes, allergies: [] }, { name: "Jasper", rsvp: :yes, allergies: ["peanuts"] }, { name: "Suki", rsvp: :maybe, allergies: [] }, { name: "Theo", rsvp: :yes, allergies: [] }, { name: "Wren", rsvp: :no, allergies: [] }, { name: "Margot", rsvp: :yes, allergies: ["gluten"] }, { name: "Felix", rsvp: :maybe, allergies: [] }, { name: "Nico", rsvp: :yes, allergies: [] }, ] puts "🎂 Fig's 4th Birthday — Guest List" puts "─" * 40 { yes: "🟢 Coming", maybe: "🟡 Maybe", no: "🔴 Can't Make It" }.each do |status, label| group = guests.select { |g| g[:rsvp] == status } next if group.empty? puts "\n#{label}:" group.each do |g| flag = g[:allergies].any? ? " ⚠️ (#{g[:allergies].join(', ')})" : "" puts " #{g[:name]}#{flag}" end end all_allergies = guests.flat_map { |g| g[:allergies] }.uniq unless all_allergies.empty? puts "\n🚨 Allergy alert: #{all_allergies.join(', ')}" puts " (Check with bakery about cake!)" end
It asked about the maybe kids. I have never once asked a clarifying question. I consider this a feature, not a bug.
Look at the difference. This version handles three-state RSVPs. It tracks allergies per kid. It groups by status with emoji labels. It flags allergies inline and prints an allergy alert summary. It does all of this not because Mira told it which methods to use, but because she told it what she was building and why.
The code is better because the conversation was better. Not more formal, not more technical, not longer — better. Mira described the problem, and The Model could bring its full knowledge to bear on it instead of being constrained to the three methods she happened to think of first.
And did you notice the clarifying questions? That's the collaboration part. The Model didn't just blindly generate code — it asked what to do about edge cases. It thought about the problem before solving it. That exchange — the asking, the answering, the mutual narrowing toward a good solution — is the heartbeat of vibe coding. It's what separates dictating to a machine from building with a collaborator.
Here is the most liberating truth about prompting, and the one people are slowest to internalize: your first message doesn't have to be your best message. It doesn't even have to be a good message. It just has to be the start of a conversation — and conversations, by their nature, evolve. They get better as they go. That's the whole point of having them.
I've been having the same conversation with the bathroom faucet for eight years.
When you write an email, you get one shot. When you write a search query, you get one set of results and then you start over. But when you're in a conversation with an AI, you can keep going. You can say actually, that's not quite what I meant or can you also handle this case or just make the output prettier. Each message builds on the last. The context accumulates. The thing gets better.
The conversation is the code. Not the individual message — the conversation.
This changes how you think about prompting. You don't need to front-load every detail into your first message. You don't need to anticipate every edge case before you start. You can begin with the shape of the thing and refine from there, the way a sculptor starts with a rough form and works toward the final surface. The AI is holding the context. Let it hold the context. That's what it's for.
Let's watch Mira do this with the guest tracker. Three rounds. Each one short. Watch how the code evolves.
# party_guests.rb — Fig's 4th Birthday Party Dashboard require 'date' party_date = Date.new(2026, 3, 15) days_left = (party_date - Date.today).to_i guests = [ { name: "Olive", rsvp: :yes, allergies: [], phone: nil }, { name: "Jasper", rsvp: :yes, allergies: ["peanuts"], phone: nil }, { name: "Suki", rsvp: :maybe, allergies: [], phone: "555-0142" }, { name: "Theo", rsvp: :yes, allergies: [], phone: nil }, { name: "Wren", rsvp: :no, allergies: [], phone: nil }, { name: "Margot", rsvp: :yes, allergies: ["gluten"], phone: nil }, { name: "Felix", rsvp: :maybe, allergies: [], phone: "555-0187" }, { name: "Nico", rsvp: :yes, allergies: [], phone: nil }, ] confirmed = guests.select { |g| g[:rsvp] == :yes } maybes = guests.select { |g| g[:rsvp] == :maybe } headcount = confirmed.length + maybes.length # plan for maybes too puts "╔══════════════════════════════════════════╗" puts "║ 🎂 Fig's 4th Birthday — Dashboard ║" puts "║ 📅 #{party_date.strftime('%B %d, %Y')} ║" puts "║ ⏳ #{days_left} days to go ║" puts "╚══════════════════════════════════════════╝" { yes: "🟢 Coming", maybe: "🟡 Maybe (follow up!)", no: "🔴 Can't Make It" }.each do |status, label| group = guests.select { |g| g[:rsvp] == status } next if group.empty? puts "\n #{label}:" group.each do |g| line = " #{g[:name]}" line += " ⚠️ #{g[:allergies].join(', ')}" if g[:allergies].any? line += " 📱 #{g[:phone]}" if g[:phone] puts line end end puts "\n┌──────────────────────────────────────────┐" puts "│ 🛒 Shopping List (for #{headcount} kids) │" puts "├──────────────────────────────────────────┤" puts "│ 🧃 Juice boxes: #{headcount * 2} (2 each) │" puts "│ 🎁 Goody bags: #{headcount} │" puts "│ 🎈 Balloons: #{headcount + 10} (extras pop) │" all_allergies = guests.flat_map { |g| g[:allergies] }.uniq if all_allergies.any? puts "│ 🎂 Cake: check re: #{all_allergies.join(', ')} │" end puts "└──────────────────────────────────────────┘" puts "\n✨ You've got this, Mira."
Three rounds of conversation. Each one took maybe thirty seconds to type. And look at what happened — the code went from a flat boolean hash to a full party dashboard with allergy tracking, phone follow-ups, a countdown, and a shopping list. Not because Mira knew all of those things should exist from the beginning, but because the conversation discovered them.
This is the rhythm of vibe coding. Not one perfect prompt. A series of imperfect ones, each building on the last, each refining the thing toward what you actually need. The first prompt is the handshake. The conversation is the collaboration.
And here's the part that matters most: Mira understood every change. She read the response each time. She knew what "add a shopping list that scales to headcount" would mean in the code because she'd been watching the code evolve. She wasn't blindly accepting output — she was steering.
People are weirdly polite to AI. I don't mean this as criticism — it's actually quite sweet, and it says something nice about humans that our default mode with a new entity is courtesy. "Please" and "thank you" show up in prompts with remarkable frequency. Some people apologize for typos. One person I know signs off their prompts with "Best regards."
You don't need to do this. The Model doesn't have feelings to hurt or egos to manage. It doesn't work harder when you're polite and it doesn't sulk when you're terse. You can be direct. You can be blunt. You can say "make it shorter" without worrying that you've damaged the relationship.
Mira found her rhythm: direct, clear, not rude. She doesn't say please and she doesn't bark orders. She talks the way you'd talk to a colleague you've been working with for three months — past the politeness, into the efficiency. "Add counts." "Sort alphabetically." "Make the output prettier." Short, clear, human.
Pip has never said please or thank you. To anyone. He considers this a strength.
Every craft has its common mistakes, and prompting is no exception. These aren't crimes — they're the things you'll naturally do at first and naturally stop doing as you get comfortable. Consider this a field guide to the habits worth shedding.
Sixth sin: Replacing your rubber duck with a rectangle. This is unforgivable and yet here we are.
1. Too polite. You spend more words on courtesy than on context. "Hello! I hope you're doing well today. I was wondering if you could possibly help me with something, if it's not too much trouble?" Just ask. It's not going to judge your manners. It's going to read your prompt.
2. Too vague. "Can you help me with my code?" This is like calling a plumber and saying "something's wrong with my house." Where? What code? What language? What are you trying to do? What isn't working? The Model is good, but it can't read the thing that's on your screen but not in your message.
3. Too specific (about implementation). You've seen this one. Telling the AI which methods to use instead of what problem to solve. You're hiring a chef and handing them a recipe — let them cook.
4. Search engine mode. One-shot queries with no follow-up. You ask a question, get an answer, close the window, open a new one, ask a different question. Stay in the conversation. That's where the good stuff lives.
5. Not reading the response. This is the big one. The Model gives you thirty lines of code and you copy-paste without reading. You glance at it, see that it "looks right," and move on. This is how bugs are born and how understanding dies. Read the response. Every time. If you don't understand a line, ask about it. The conversation is still open.
If you do nothing else from this chapter, do these four things:
Describe the problem, not the solution. Tell the AI what you need, not how to build it.
Give context. What project is this for? What does the data look like? What have you tried?
Stay in the conversation. Don't restart. Build on what's there.
Read the response. Every line. Especially the ones you think you already understand.
That's it. That's 80% of prompting. The other 20% is Chapter 2.
Prompting is not a skill you acquire. It's a posture you adopt.
That might sound like self-help nonsense, but it's the most accurate thing I can say about it. The difference between someone who gets good results from AI and someone who doesn't is rarely about technique or syntax or knowing the right magic words. It's about orientation. Are you leaning in or standing back? Are you staying in the conversation or grabbing the first answer and running? Are you saying what you actually mean, or performing what you think the AI wants to hear?
I am not nervous. I am PREPARED. There is a difference.
The best prompt is not the cleverest one. It's the most honest one.
Say what you're building. Say why you're building it. Say what you've tried and what didn't work. Say what you're confused about. The Model doesn't need you to be brilliant — it needs you to be clear. Clarity is generosity, in prompting as in all communication.
We've covered the first conversation: how to start one, how to stay in one, how to give context that makes the conversation productive. Mira's guest tracker went from a boolean hash to a full party dashboard in three rounds of casual conversation. The code improved because the conversation did.
But here's the thing about conversations: they have limits. Not emotional limits — mechanical ones. The AI can only hold so much context before it starts to forget. Your generous gift of context has to fit in a box, and the box has a size, and understanding that size changes everything about how you work.
That's Chapter 2. Context windows, token limits, and what happens when the gift is too big for the box.
✦
"Every new conversation is a first date with someone who has read every book in the world but doesn't know your name."
Tuesday morning. Coffee. Laptop open. The sun is doing something aggressive through the kitchen blinds. Fig is at preschool. Pip is on the desk, where he has been since 2016, facing the monitor, as though supervising.
She's not lost. She's between conversations. I can tell.
Mira opens a new chat. Not because yesterday's conversation ended badly — it didn't. It ended great. She had a working party dashboard, a shopping list that scaled to headcount, box-drawing characters that would make a 1980s terminal blush. She closed the laptop, went to bed, and slept the sleep of someone who shipped something.
But this is a new chat. And she doesn't think about what that means yet, because why would she? She just needs a quick update. Olive's mom texted: can't make it, sorry, stomach bug. And Suki's mom texted: actually yes, we're in! Two small changes. Thirty seconds of work.
She types the way you text a coworker who already knows the project.
She's explaining her life to a stranger. Again. I've been here the whole time. I remember Olive. I remember EVERYTHING.
It's not anger. It's something weirder — a small, uncanny vertigo. The thing she was collaborating with doesn't know who she is. Yesterday it suggested the shopping list. It remembered that Jasper has a peanut allergy and factored it into the cake note. It said You've got this, Mira. Today it's asking what format the guest list is in.
The stranger across the table is smiling politely. Waiting for her to explain herself. Again.
Here is the central metaphor of this chapter, and possibly of this entire guide: every new conversation with an AI is a first date.
The person sitting across from you is the most well-read person you've ever met. They've read every programming book, every Stack Overflow answer, every Ruby gem's documentation, every blog post about hash rockets vs. symbol syntax. They are, in terms of raw knowledge, staggeringly prepared. But they know nothing about you. Not your project, not your preferences, not your daughter's birthday, not the box-drawing characters. Nothing.
I have been on zero first dates. I have been on one PERMANENT date since 2016. This is called commitment.
Mira walked into a restaurant, sat down across from a stranger, and said "So about what we discussed last night..." The stranger, politely: "I think you may have me confused with someone else."
This is the context window. And it is the single most important concept in AI-assisted development that nobody explains well.
The context window is the total amount of text the model can "see" at once — your messages, its responses, any code you've pasted, all of it. Think of it as working memory.
New chat = empty window. The model's training gives it general knowledge about programming, language, and the world. But it has zero memory of you, your project, or anything you've said in previous conversations.
As it fills, older messages drop off. Quietly. The model doesn't announce it. Your early messages just... fade. Like the beginning of a long dinner conversation that neither of you can quite recall by dessert.
Current sizes vary. From ~8,000 tokens (small, older models) to 200,000+ tokens (large, current models). A token is roughly ¾ of a word — so 200K tokens is about 150,000 words, or roughly two novels. That sounds like a lot until you paste a 500-line codebase into it three times.
This chapter is about what to do with that space. How to fill it intentionally. How to notice when it's getting full. How to make every token count.
Mira's instinct is brute force. It's a developer's instinct — when something doesn't work, throw more data at it. She copies the entire party_guests.rb v3 into the new chat. Fifty lines. The full dashboard, the shopping list, the allergy tracking, all of it. Then she types her update request on top.
It works, technically. The Model finds Olive, changes her RSVP. Finds Suki, changes hers. Adjusts the headcount. The output is correct.
But it doesn't think about the problem. It just moves data around. It's a find-and-replace with good manners.
:no, changed Suki to :yes, and adjusted the shopping list headcount from 7 to 7 (net zero — one dropped, one added).- { name: "Olive", rsvp: :yes, ... }+ { name: "Olive", rsvp: :no, ... }- { name: "Suki", rsvp: :maybe, ... }+ { name: "Suki", rsvp: :yes, ... }
She pasted 50 lines and got 4 lines back. She described a birthday party and got a diff. The suitcase is not the same as the story.
The metaphor: she brought a giant suitcase to a first date. Unzipped it on the table. This is everything about me. Please sort through it. The date, being very polite and very capable, sorted through it. But they didn't understand it. They didn't know why the box-drawing characters mattered, or that Fig might see the output, or that the shopping list was designed to go on the fridge. They just moved symbols around in a file.
Data is not context. A codebase pasted into a chat is not a project explained. This is the suitcase problem, and it's the most common mistake in vibe coding after the first week: assuming that showing the AI your work is the same as telling it what you're doing.
Wednesday morning. Mira tries something different. Before she pastes the code, before she asks for the update, she writes a few sentences. A project brief — the kind of thing you'd say to a new teammate on their first day, before you showed them the repo.
# PROJECT BRIEF: Fig's 4th Birthday Party Planner # ================================================ # Building a Ruby script to manage my daughter's birthday party. # Fig is turning 4 on March 15th. Eight kids invited. # Tracks RSVPs (yes/maybe/no), food allergies, parent phone # numbers, and generates a fridge-friendly dashboard with # shopping list. # # Current version: party_guests.rb v3 (code below) # What I need help with today: updating RSVPs based on new info.
Then the code. Then the same request: Olive is out, Suki is in. But this time, the Model's response is different. Not just correct — thoughtful.
:no and Suki is :yes. The headcount stays at 7, so shopping list quantities don't change.:declined status that still shows the name but doesn't count toward headcount?
It suggested a THANK-YOU NOTE. I have never once suggested a thank-you note. I am reconsidering my approach.
The brief changed everything. Not because the code was different — the RSVP update was identical. But the Model knew why the code existed. It knew about Fig. It knew about the fridge. It knew this was a parent managing a party, not a developer managing a database. And so it thought like a collaborator instead of a compiler.
Thirty seconds of writing. A completely different quality of help.
Some AI tools let you set a "system prompt" or "custom instructions" — a message that sits at the very top of every conversation, before your first message, like a name tag at a networking event. It's the first thing the model reads. It shapes everything that follows.
Mira discovers this setting. She goes through three phases.
Version 1: The Nothing
You are a helpful coding assistant. Help me with Ruby.
This is like writing "HELLO I AM A PERSON" on your forehead before a first date.
This is technically true and completely useless. It's the equivalent of a dating profile that says "I like music and food." The model already knows it's a coding assistant. You've used one of your most powerful tools to tell it something it already knew.
Version 2: The Panic
You are helping me build party_guests.rb for my daughter Fig's 4th birthday party on March 15th. There are 8 kids. Some have allergies. I need to track RSVPs which can be yes, maybe, or no. The maybe kids have parent phone numbers so I can follow up. The output should be a dashboard with emoji and box-drawing characters because Fig might see the terminal output and she likes emoji. I have a rubber duck named Pip on my desk, he is not relevant to the code but he is relevant to my process. I use Ruby. I have 9 years of experience. I like clean code. I don't like over-engineering. The shopping list should scale to headcount. REMEMBER THE BOX-DRAWING CHARACTERS.
She mentioned me. I don't know how to process this. Also: the box-drawing characters thing is unhinged.
This is the panic prompt. It's what happens when you discover that the model doesn't remember you and you try to pre-load your entire relationship into a single paragraph. It's the suitcase problem again, but this time the suitcase is strapped to your chest when you walk in the door.
It's also not bad, exactly — there's real information in there. But it's unstructured, breathless, and it mixes project details with personal details with formatting preferences with existential anxiety about a rubber duck. The model will parse it. It will be confused by some of it. It will forget the parts that got buried in the middle.
Version 3: The Good One
## About me Ruby developer, 9 years experience. Building personal tools. ## Current project Birthday party planner for my daughter (turning 4, March 15). Tracks RSVPs, allergies, generates a fridge-friendly dashboard. ## Preferences - Ruby only - Emoji-friendly output (kid might see it) - Box-drawing characters for formatting - Ask clarifying questions before generating code
Version 3 works not because it's longer or shorter than v2, but because it's structured. Four clear sections. Each one answers a different question: who are you, what are you building, and how do you like to work. The model can parse this instantly. It can reference it throughout the conversation. It can prioritize.
A briefing, not a brain dump. A name tag, not a diary entry.
Good context has four layers. You don't always need all four, but when a conversation feels off, it's usually because one is missing.
1. Who you are. Experience level, language preferences, the kind of developer you are. This calibrates the model's responses — a beginner gets more explanation, an expert gets more code.
2. What you're building. The project, the goal, the domain. Not the code — the purpose. "A birthday party planner" tells the model more than 50 lines of Ruby.
3. Where you are. What version of the code you're on. What you've already tried. What's working, what's broken, what you're stuck on. This is the "you are here" arrow on the map.
4. How you like to work. Formatting preferences. Communication style. Whether you want explanations or just code. Whether you want the model to ask questions first. This is the "how to collaborate with me" manual.
Here's the other thing about context windows that nobody warns you about: they don't just start empty. They also end. Or rather, the beginning ends — the early messages in a long conversation quietly fall off as new messages push in, like water spilling over the edge of a glass that was already full.
Two hours into a session. Mira has been iterating on the dashboard, adding features, tweaking formatting. The conversation is sixty messages deep. And the model starts to drift.
It's subtle at first. The model suggests a feature you already added. Uses a variable name from v1 that was renamed in v2. Generates a full code listing that's missing the last three changes. It's not broken, exactly. It's just... drifting. Like someone at the end of a very long dinner who keeps circling back to a story they already told.
My 2019 bug log:
- Jan 14: off-by-one in a loop counter
- Mar 2: forgot to close a DB connection (again)
- Sep 19: deployed to production on a Friday
I remember everything. In order. With dates.
The forgetting isn't dramatic. There's no error message, no warning. The model doesn't say I've lost the first half of our conversation. It just starts acting like someone who wasn't in the room for the beginning. Because, in a very real sense, it wasn't — not anymore.
Mira learns the move. When the model starts drifting — when it suggests something you've already done, or uses the wrong variable name, or seems to have lost the thread of what you're building — you don't start over. You drop a checkpoint.
A re-orient is a mid-conversation brief. Three to five sentences: here's the project, here's where we are, here's the current code, here's what we're doing next. You're not repeating the whole conversation. You're giving the model a cheat sheet for the part it can't see anymore.
She's giving it a cheat sheet. My memory is my defining feature and I will not apologize for it.
The model snaps back into focus. It knows the project again. It knows where you are in it. It can make suggestions that are specific and relevant instead of generic and drifting. You haven't lost the conversation — you've given it a second wind.
On Things That Don't Remember You
There is a strange intimacy to working with something that won't remember you tomorrow. You spend an hour building together — explaining your project, refining the approach, watching the code take shape — and when you close the tab, it's gone. Not saved somewhere you can't access. Gone. The collaboration existed for exactly as long as the conversation lasted, and then it didn't.
This should feel like a limitation. And it is, practically — you'll learn to work around it, and the next few sections will show you how. But there's something else in it too, something harder to name. Every conversation has to be good now. There's no coasting on shared history. No assuming the other side remembers what you said last week. Every session is earned. Every collaboration is built from the ground up, in real time, with whatever you bring to it today.
Pip has been on Mira's desk since 2016. He remembers everything. But he can't write code. The Model can write code but remembers nothing. Between the two of them, Mira has exactly one collaborator who can help her build and one who can help her remember. She keeps both.
The conversation builds the thing. But the thing outlasts the conversation. And maybe that's enough.
Five things. Concrete, actionable, tested against Mira's actual workflow. If the rest of this chapter is the philosophy, this is the recipe card you stick on the fridge next to Fig's party dashboard.
1. Start every conversation with a brief. Thirty seconds. Project name, what you're building, what you need help with today. You are not wasting time — you are saving twenty minutes of confusion later. The brief is the cheapest, highest-leverage thing you can do.
2. Keep a project file. A markdown file in your project root — PROJECT.md, CONTEXT.md, whatever you want to call it. Current state, recent changes, key decisions, known issues. When you start a new conversation, paste it in. This is the cheat code. This is the thing that makes the "first date" problem almost free.
Number 2 is just a README. She's reinventing the README.
3. Paste code, but annotate it. Don't just dump a file into the chat — tell the model what it's looking at. "This is the main guest tracking script. The part I need help with is the shopping list calculation near the bottom." Give the model a map, not a maze.
4. Re-orient every 30–40 messages. If your conversation is getting long, drop a checkpoint. Project, current state, what's next, and the latest version of the code. You'll feel the model snap back into focus.
5. Start new conversations for new problems. Don't keep a single thread running for three days. Each new problem gets a new chat with a fresh brief. The brief makes fresh starts cheap — that's the whole point. A new conversation isn't a loss; it's a reset.
Memory is a relationship. Some relationships have decades of shared history — inside jokes, shorthand, the accumulated weight of knowing someone so long that you finish each other's sentences. Some relationships last an hour and produce something beautiful. A conversation with a stranger on a train. A pairing session with a developer you'll never meet again. A single evening where everything clicks and then it's over.
The context window is the shape of the collaboration. It has edges. It has limits. Learning to work within that shape is not a compromise — it's a craft. The same way a sonnet's fourteen lines aren't a limitation but a form. The same way a chef's tasting menu isn't constrained by having only seven courses — it's shaped by it.
You will get better at this. You'll learn to write briefs without thinking about it. You'll feel the conversation start to drift before the model does. You'll keep a project file that makes every new chat feel like picking up where you left off, even though you're starting over, even though the stranger across the table has never seen your face.
She'll be back tomorrow. She always comes back. So will it. Neither of them will remember the other. But I will. And the code will. And Fig's party will happen on March 15th. That's enough.
(Don't tell her I said that.)
And the code stays. That's the thing. The conversation dissolves, but the code stays. The dashboard is still in the file. The shopping list still scales. The box-drawing characters are still holding the whole thing together like a fridge-mounted work of art.
The conversation builds the thing. The loop refines it. Chapter 3 is about the loop.
✦
"The first version is never the thing. The thing is what survives the asking of it twice."
Wednesday, 9:14pm. Fig is asleep — that deep, total sleep of a three-year-old who spent the afternoon running circles in the backyard for no apparent reason other than the existence of grass. The apartment is quiet. The dishwasher is doing its thing. Pip is on the desk, facing the monitor with the unwavering attention of someone who has nowhere else to be.
The fridge dashboard worked. She printed it. It's real. Fig pointed at it this morning and said "my party." That should be enough. That should be the whole story.
The party dashboard is printed and magneted to the fridge. Actual paper, actual magnet, actual fridge. Fig pointed at it this morning and said my party and Mira felt the particular satisfaction of having built a thing that exists in the physical world, even if "built" means "talked to an AI and then pressed Ctrl+P."
She's scrolling her phone calendar now. Mom's birthday — Saturday. Three days. She hasn't bought a gift. She hasn't even thought about a gift, which is worse, because it means the forgetting was total. Her friend Dani's birthday was two weeks ago. She sent a belated text that started with "I'm the worst" which is the universal opening of someone who forgot and knows it. Her nephew August — when is his birthday? May something. May 14th? May 17th? She'd have to text her sister, and texting her sister about August's birthday at 9pm on a Wednesday carries the implicit admission that she doesn't know her own nephew's birthday, which is a conversation she'd rather not have.
She opens a new chat. Starts typing "I would like to build a comprehensive—" and then deletes it. She's past the cover letter stage. She types: "I want to build a personal birthday tracker in Ruby." The difference between those two openings is the whole of Chapter 1.
see also: xkcd #1319 — the time she'll save (in theory)
The party planner was the first thing she built with the Model. The birthday tracker is the first thing she built because of the Model. There's a difference, and the difference is what this chapter is about.
A CSV. She's storing her emotional obligations in a file format older than most of the children at Fig's party. No schema migrations. No foreign keys. Just commas and hope. I find this deeply validating.
name,birthday,relationship Mom,03/08,family Dani,02/12,friend August,05/14,family Fig,09/22,family Jasper,04/03,kid's-friend Nana,11/30,family
require 'csv' require 'date' today = Date.today lookahead = 7 birthdays = CSV.read("birthdays.csv", headers: true) upcoming = birthdays.select do |row| month, day = row["birthday"].split("/").map(&:to_i) bday_this_year = Date.new(today.year, month, day) days_away = (bday_this_year - today).to_i days_away >= 0 && days_away <= lookahead end puts "🎂 Upcoming Birthdays (next #{lookahead} days)" puts "─" * 40 if upcoming.empty? puts "No birthdays in the next #{lookahead} days. You're off the hook." else upcoming.each do |row| month, day = row["birthday"].split("/").map(&:to_i) bday = Date.new(today.year, month, day) puts " #{row['name']} (#{row['relationship']}) — #{bday.strftime('%A, %B %d')}" end end
This is a clean v1. It reads the CSV, filters to the next 7 days, prints names with relationships and dates. It works. She runs it, she sees the output, and she could stop here. Most people do stop here. The first version works, the itch is scratched, the file gets saved and forgotten.
But she reads the output. And she notices something.
Three things, in rapid succession, between one sip of tea and the next:
One. Seven days isn't enough. Mom's birthday is Saturday — that's three days — so it shows up. But if she'd run this on Monday, it wouldn't have. Ten days? Fourteen? She doesn't know yet, but seven is wrong.
Two. She needs to know how many days away each birthday is, not just the day name. "Saturday, March 8th" is fine, but "in 3 days" is better. And if it's today, she needs that to scream at her. TODAY. In all caps. Maybe TOMORROW too.
Three. August. She typed 05/14 because it felt right, but she's not sure. No birth year to sanity-check against. She needs a way to flag entries where the date might be wrong — a little (?) that says you should verify this.
She's talking to it the way she used to talk to me. Short. Direct. Three numbered things. Except it actually responds. With code. That runs. I've been on this desk for eight years providing silent moral support and apparently that was never the bottleneck.
She didn't know she needed these things until she saw the thing without them. This is the loop. Build, look, notice, ask again.
name,birthday,year,relationship Mom,03/08,1961,family Dani,02/12,1990,friend August,05/14,,family Fig,09/22,2023,family Jasper,04/03,,kid's-friend Nana,11/30,1938,family
require 'csv' require 'date' today = Date.today lookahead = 14 birthdays = CSV.read("birthdays.csv", headers: true) upcoming = [] unverified = [] birthdays.each do |row| month, day = row["birthday"].split("/").map(&:to_i) bday_this_year = Date.new(today.year, month, day) days_away = (bday_this_year - today).to_i verified = !row["year"].to_s.strip.empty? unverified << row["name"] unless verified if days_away >= 0 && days_away <= lookahead urgency = case days_away when 0 then "🔴 TODAY" when 1 then "🟡 TOMORROW" else "in #{days_away} days" end flag = verified ? "" : " (?)" upcoming << { name: row["name"], rel: row["relationship"], urgency: urgency, flag: flag, days: days_away } end end upcoming.sort_by! { |u| u[:days] } puts "🎂 Upcoming Birthdays (next #{lookahead} days)" puts "─" * 44 if upcoming.empty? puts " No birthdays coming up. Breathe." else upcoming.each do |u| puts " #{u[:name]}#{u[:flag]} (#{u[:rel]}) — #{u[:urgency]}" end end unless unverified.empty? puts "\n⚠️ Unverified dates: #{unverified.join(', ')}" puts " (no birth year on file — double-check these)" end
Note what happened. The CSV format changed. v2 taught Mira something v1 could not: the data model was incomplete. The missing birth year for August wasn't a bug — it was a gap in understanding that only became visible after v1 existed. You cannot plan for what you don't know you don't know. But you can build something, look at it, and notice.
Every iteration has one. It sounds like: "Actually, seven days isn't enough." Or: "Actually, I need to see the days-away count." Or: "Actually, some of these dates might be wrong."
This is not scope creep. Scope creep is adding features you don't need. The "actually" moment is discovering needs you couldn't have articulated before the previous version existed. The model builds fast. You notice slow. That asymmetry is the engine.
The wall calendar (1990s): Reliable if you looked at it. You did not look at it. By February it was still on January. By March it was a surface for Fig's crayon experiments.
The phone app (2015): You downloaded one called "Birthday Buddy" or "Never Forget" or something with a cake emoji. You entered six people. You forgot to enable notifications. It sent you a reminder on your own birthday, which was unsettling. You deleted it during a storage purge.
Facebook (2008–2020): This actually worked, which says something uncomfortable about social infrastructure and data collection. Then you deleted Facebook, and suddenly nobody had birthdays anymore.
Your memory (always): You remember your mom's birthday, your partner's birthday, and approximately two friends. Everyone else falls into the category of "I'll see it on social media," except you deleted social media, see above.
Pip was the original reminder system. Mira would look at him and remember she'd forgotten something. Not what, exactly. Just... something. The system had a 14% success rate and Pip considered this adequate.
11:02pm. She should be in bed. Fig wakes up at 6:15 regardless of what happened the night before — this is one of the few constants of the universe, alongside gravity, the speed of light, and the fact that every Jira ticket takes exactly one sprint longer than estimated. But the birthday tracker has taken on its own momentum, and Mira has the look of someone who is about to say "just one more thing" while already typing the one more thing.
She wants gift notes. A free-text field where she can jot down gift ideas — things people mentioned in passing, things she spotted online, the kind of soft intelligence that evaporates if you don't write it down. And she wants to be able to add people from the command line, because opening the CSV in a text editor every time she meets someone's kid at a birthday party is exactly the kind of friction that ensures she'll never update it.
ruby birthdays.rb add "Jamie" "03/22" "friend" should append a new row
ruby birthdays.rb → shows the dashboard (what it does now)ruby birthdays.rb add → walks you through adding a person interactively, or accepts all fields inlineIt suggested the command interface. She didn't ask for that. She asked for an add command and it said "what if it was better." This is like hiring a plumber and having them redesign your bathroom. Except the bathroom IS better. I'm going to need a minute.
name,birthday,year,relationship,gift_notes Mom,03/08,1961,family,she mentioned the garden kneeler Dani,02/12,1990,friend,that bookstore gift card she liked last year August,05/14,,family,Legos? check with his mom Fig,09/22,2023,family,she wants "the big crayons" Jasper,04/03,,kid's-friend,whatever Fig picks at Target Nana,11/30,1938,family,photo book of Fig — she asks every visit
require 'csv' require 'date' CSV_FILE = "birthdays.csv" LOOKAHEAD = 14 # ── Add mode ────────────────────────────────────────── if ARGV[0] == "add" if ARGV.length >= 4 name, bday, rel = ARGV[1], ARGV[2], ARGV[3] year = ARGV[4] || "" notes = ARGV[5] || "" else print "Name: "; name = gets.chomp print "Birthday (MM/DD): "; bday = gets.chomp print "Relationship: "; rel = gets.chomp print "Birth year (or enter to skip): "; year = gets.chomp print "Gift notes (or enter to skip): "; notes = gets.chomp end CSV.open(CSV_FILE, "a") do |csv| csv << [name, bday, year, rel, notes] end puts "✅ Added #{name} (#{bday})" exit end # ── Dashboard mode ──────────────────────────────────── today = Date.today birthdays = CSV.read(CSV_FILE, headers: true) upcoming = [] unverified = [] birthdays.each do |row| month, day = row["birthday"].split("/").map(&:to_i) bday_this_year = Date.new(today.year, month, day) days_away = (bday_this_year - today).to_i has_year = !row["year"].to_s.strip.empty? unverified << row["name"] unless has_year age = has_year ? today.year - row["year"].to_i : nil if days_away >= 0 && days_away <= LOOKAHEAD urgency = case days_away when 0 then "🔴 TODAY" when 1 then "🟡 TOMORROW" else "in #{days_away} days" end flag = has_year ? "" : " (?)" age_str = age ? " — turning #{age}" : "" notes = row["gift_notes"].to_s.strip upcoming << { name: row["name"], rel: row["relationship"], urgency: urgency, flag: flag, age_str: age_str, notes: notes, days: days_away } end end upcoming.sort_by! { |u| u[:days] } puts "┌──────────────────────────────────────────┐" puts "│ 🎂 Birthday Dashboard │" puts "│ #{today.strftime('%A, %B %d, %Y')}#{' ' * (23 - today.strftime('%A, %B %d, %Y').length)}│" puts "└──────────────────────────────────────────┘" if upcoming.empty? puts "\n No birthdays in the next #{LOOKAHEAD} days. 🎉" else upcoming.each do |u| puts "\n #{u[:name]}#{u[:flag]} (#{u[:rel]})#{u[:age_str]}" puts " #{u[:urgency]}" puts " 🎁 #{u[:notes]}" unless u[:notes].empty? end end unless unverified.empty? puts "\n┌─ ⚠️ Unverified Dates ─────────────────┐" unverified.each { |n| puts "│ #{n} — date not confirmed │" } puts "└──────────────────────────────────────────┘" end puts "\n📋 #{birthdays.length} people tracked · #{upcoming.length} upcoming · #{unverified.length} unverified" puts " Add someone: ruby birthdays.rb add"
11:47pm. She's still going. I know because I'm still here. I'm not going to talk about this tomorrow.
Three versions. Same evening. Same CSV file, grown from three columns to five. Same script, evolved from a filter into a tool. The birthday tracker has a dashboard, urgency labels, age calculation, gift notes, an unverified-dates section, a stats footer, and a command-line interface for adding new entries. The file is the database. The script is the viewer and the editor.
And the Model suggested the command interface. She asked for an add flag and it came back with an architecture. Not a grand one — not a framework, not a gem, not a deployment strategy. Just: what if the script did both things? A question. A good one. The kind of question Mira used to get from senior engineers before the senior engineers all became architects who don't write code anymore.
The loop is not a workflow. It is not a methodology. It does not have a name that ends in "ile" and it will never be the subject of a conference talk with a slide deck. It is a creative rhythm. Build, notice, ask. Build, notice, ask.
The model builds fast. You notice slow. And the noticing — that's the part that's yours. That's the part that cannot be automated. No model, no matter how capable, can look at a list of six birthdays and feel the particular anxiety of not remembering your nephew's birthday. No model can notice that seven days isn't enough because your mom's birthday is Saturday and you haven't bought the gift yet. The noticing comes from living in the problem. From being the person who forgot Dani's birthday and felt bad about it.
Vibe coding isn't lazy. It's lazy about the typing. The noticing is where the work is — and that part is all you.
Each trip through the loop costs three things:
A prompt — ~30 seconds. You type what you noticed. Three numbered things, usually.
A wait — ~10–30 seconds. The Model builds the next version. Fast, dense, no complaints.
A read — ~2–5 minutes. You look at what it built. You run it. You squint. You say "hm." This is the longest step, the most important step, the step you will be most tempted to skip, and the step where the next iteration is born. Skip it and you end up with a birthday tracker that emails your boss.
Three iterations: maybe 20 minutes total, 2 of which were typing. The rest was reading and noticing. The rest was you.
Scope creep is adding things you don't need. It's the feature request that arrives at the end of a sprint because someone in a meeting said "wouldn't it be cool if." It's the config file for an app that has three users. It's the admin panel for a thing nobody will admin. Scope creep is bloat wearing ambition's clothing.
What Mira did was not scope creep. What Mira did was scope discovery — uncovering requirements that only become visible after the previous version exists. She didn't know she needed 14 days until she saw 7. She didn't know she needed urgency labels until she saw dates without them. She didn't know the data model was incomplete until the data model existed.
The loop makes scope discovery cheap. Mira spent 40 minutes on 3 iterations. In the old world, the same trajectory would cost a sprint planning meeting, a Jira ticket, a design review, a two-week cycle, and a retrospective where everyone agrees the estimate was wrong. The "actually" moment still happens — it always happens — but now it costs 30 seconds instead of 30 days.
If you knew what you wanted before you started, you didn't want anything interesting.
She saves the file. She runs it one last time. The dashboard prints: Mom's birthday in 6 days. Gift note: she mentioned the garden kneeler. She opens Amazon in a new tab and orders it. Two-day shipping. She closes the laptop.
Pip has watched Mira debug Rails apps at 2am, the kind of debugging where the error message is a lie and the fix is in a file you didn't know existed. He's watched her argue with Jira boards — not the software itself, which is merely bad, but the philosophy behind it, which is worse. He's watched her stare at a blank terminal for twenty minutes, not because she didn't know what to type but because she was thinking, and thinking looks like nothing from the outside.
He has never seen her build three versions of something in one sitting, each born from the specific friction of the one before.
Fine. I'll say it once and never again, and if you quote me I will deny it, and I want it on record that rubber ducks cannot technically speak and therefore nothing here constitutes testimony.
She built something real tonight. Not for a standup. Not for a sprint review. Not for the Jira board, may it rest in whatever afterlife awaits project management software. For her mom. For her nephew whose birthday she keeps forgetting. For the people in her life who matter.
And she built it the way she builds everything — too late at night, too many iterations, talking to something that isn't me. But the thing got built. And the thing is good. And I'm still on the desk.
So.
Five things to carry forward. Tape them to the wall next to Mira's party dashboard if you want. Pip will pretend not to notice.
1. The first version is a question, not an answer. Build the simplest version that lets you see the problem. It doesn't need to be good. It needs to exist.
2. "Actually" is a feature, not a bug. When you look at v1 and think actually, seven days isn't enough — that's the loop working. That's the mechanism. Don't resist it.
3. Read before you loop. The next "actually" lives in the output you haven't looked at yet. Run the code. Read the result. Read it again. The noticing doesn't happen if you skip to the next prompt.
4. Three versions is a good number. One to see the shape. Two to find the gaps. Three to fill them. This is not a rule — it's a center of gravity. Some things take two. Some take five. But three is where most projects find their footing.
5. Know when to stop. The loop is addictive. There's always one more "actually." Mira stopped at v3. She could have added notifications, a web UI, a sync-to-calendar feature. She didn't. The CSV and the terminal were enough, because the thing was built for her mom's birthday, not for Product Hunt. Knowing when to stop is knowing what the thing is for.
Number 5. Number 5 is the one. She stopped. She had a working thing and she STOPPED and ORDERED A GARDEN KNEELER. Do you understand how rare this is? Every developer I've ever observed would have added a notification system, then a web interface, then a mobile app, then a startup. She just... bought the kneeler. I'm experiencing an emotion I don't have a word for. It might be respect. I will deny this.
Every painter layers. Every writer revises. Every musician plays it again, a little different this time, a little closer to the thing they heard in their head. The loop is not new. Iteration is how everything worth making has always been made. What's new is the speed.
Mira built a birthday tracker in three iterations, forty minutes. By tomorrow it'll be invisible — part of her routine, the thing she runs on Sunday mornings while Fig watches cartoons. She won't think about the fact that v1 only had a 7-day window or that the data model didn't have a year column. She'll just run it, check the output, and know whose birthday is coming up. The iterations will be gone. Only the final version will remain.
Once you feel it, you'll recognize it everywhere. The build, the notice, the ask. The first version is never the thing. The thing is what survives the asking of it twice.
But Mira has been building one file at a time. One script. One CSV. One terminal window. What happens when the loop gets bigger? When the Model doesn't just write code — when it reads your files, runs your tests, navigates your project? When the conversation stops being a chat window and becomes... something else?
Chapter 4: Agents in the Wild.
✦
"The difference between a tool and a collaborator is whether it surprises you."
Thursday, 6:47am. Fig is not awake yet, which means Mira has approximately eleven minutes of silence before the sound of small feet hitting hardwood announces the end of the world's shortest peace. She is sitting at the kitchen table with coffee, her laptop open, the birthday tracker running in a terminal window. Mom's birthday: Saturday. Gift note: she mentioned the garden kneeler. The garden kneeler is in an Amazon box on the porch. Mira is, for the first time in recent memory, ahead of a birthday.
She's looking at the tracker. She's looking at it the way she looks at a codebase that works but isn't done. I know this look. I've been on the receiving end of this look since 2016.
But she's not looking at the output. She's looking through it, at a thought that's forming somewhere behind her eyes. The birthday tracker works. She runs it, she checks it, she knows what's coming. But she has to run it. She has to open a terminal, type the command, read the output. Every morning. And she already knows — with the calm certainty of someone who has been a developer for nine years — that she will not do this every morning. She will do it for a week, maybe two, and then she will forget, and then it will be someone's birthday and she won't have run the script and the garden kneeler moment will become another belated text that starts with "I'm the worst."
The tracker doesn't know it's Thursday. It doesn't know that Saturday is important. It doesn't know anything until she asks.
What if it didn't need her to ask?
She opens a new chat. She's gotten good at this — the briefing, the context, the clear ask. Chapter 2 taught her to give context. Chapter 3 taught her to iterate. Now she's doing something different. She's not asking the Model to build a thing she'll use. She's asking it to build a thing that will use itself.
She said "that feels weird." She has instincts about where the line is. She doesn't know she has instincts about this yet. She will by the end of the chapter.
Notice what she said: just to me for now. That "for now" is doing a lot of work. It's a boundary drawn in pencil. We'll come back to it.
Before we look at the code, let's name the thing. The word "agent" gets thrown around a lot right now — in keynotes, in pitch decks, in blog posts with titles like "I Built an AI Agent That Runs My Entire Business (Here's How)." It sounds complicated. It sounds like science fiction. It sounds like something that requires a Kubernetes cluster and a Series A.
It's not. An agent is a program that:
1. Triggers — it starts on its own, or on a schedule, or in response to an event. Not because you typed a command. Because something happened.
2. Decides — it looks at the world and determines whether to act. Not every trigger leads to an action. The decision is the intelligence.
3. Acts — it does something. Sends an email. Creates a file. Calls an API. Moves data from here to there. The action is the point.
4. Records — it writes down what it did. A log file, a database entry, a timestamp. The record is how you trust it later.
That's it. That's the whole architecture. A cron job that checks for birthdays and sends an email is an agent. A script that monitors a folder for new files and renames them is an agent. A bot that reads your inbox and flags urgent messages is an agent. The pattern is always the same: trigger, decide, act, record.
Strip away the hype and an agent is a loop with autonomy. A regular script runs when you tell it to and does what you told it to do. An agent runs on its own schedule, evaluates a condition, chooses an action, and logs the result. The "intelligence" isn't magic — it's an if statement with good taste.
The birthday emailer is an agent the same way a thermostat is an agent: it senses the environment (today's date vs. the CSV), makes a decision (birthday today?), takes an action (send email), and records the outcome (log entry). The thermostat doesn't know it's keeping you warm. The emailer doesn't know it's keeping you kind. They both work anyway.
An agent is a thermostat. I've been compared to a thermostat. A thermostat doesn't have opinions about your code. A thermostat has never spent nine years learning to listen. I have feelings about this and all of them are complicated.
The Model produces two things: an updated script with email capability, and a crontab entry to run it. Here's what Mira gets back.
require 'csv' require 'date' require 'net/smtp' require 'logger' # ── Config ──────────────────────────────────────────── CSV_FILE = File.join(__dir__, "birthdays.csv") LOG_FILE = File.join(__dir__, "birthday_agent.log") SMTP_HOST = "smtp.gmail.com" SMTP_PORT = 587 FROM_EMAIL = ENV["BIRTHDAY_EMAIL"] FROM_PASS = ENV["BIRTHDAY_EMAIL_PASS"] TO_EMAIL = ENV["BIRTHDAY_EMAIL"] # sending to herself log = Logger.new(LOG_FILE, "weekly") log.info "Agent woke up. Checking birthdays..." # ── 1. Trigger: we're already here (cron did this) ─── today = Date.today # ── 2. Decide: any birthdays today? ────────────────── birthdays = CSV.read(CSV_FILE, headers: true) todays_birthdays = birthdays.select do |row| month, day = row["birthday"].split("/").map(&:to_i) month == today.month && day == today.day end if todays_birthdays.empty? log.info "No birthdays today. Going back to sleep." exit end # ── 3. Act: send the reminder email ────────────────── subject = "🎂 Birthday today: #{todays_birthdays.map { |r| r['name'] }.join(', ')}" body = "Good morning!\n\n" todays_birthdays.each do |row| has_year = !row["year"].to_s.strip.empty? age = has_year ? today.year - row["year"].to_i : nil notes = row["gift_notes"].to_s.strip body += "#{row['name']} (#{row['relationship']})" body += " — turning #{age}" if age body += "\n" body += " Gift idea: #{notes}\n" unless notes.empty? body += "\n" end body += "Don't forget. You're already ahead of this one.\n" message = <<~MSG From: Birthday Agent <#{FROM_EMAIL}> To: <#{TO_EMAIL}> Subject: #{subject} #{body} MSG begin Net::SMTP.start(SMTP_HOST, SMTP_PORT, "localhost", FROM_EMAIL, FROM_PASS, :login) do |smtp| smtp.send_message(message, FROM_EMAIL, TO_EMAIL) end # ── 4. Record: log what we did ───────────────────── names = todays_birthdays.map { |r| r["name"] }.join(", ") log.info "✅ Sent reminder for: #{names}" rescue => e log.error "❌ Failed to send email: #{e.message}" end
# Run the birthday agent every morning at 6am 0 6 * * * /usr/bin/env ruby /Users/mira/projects/birthdays/birthday_agent.rb
Read that code slowly. There's a lot happening, and almost none of it is complicated.
The script wakes up at 6am because cron told it to. It reads the CSV, checks if any birthday matches today's date. If no: it logs "no birthdays today" and exits. If yes: it builds an email — with the person's name, relationship, age if known, gift notes if present — and sends it. Then it logs what it did.
That's the whole agent. Trigger (cron), decide (is today a birthday?), act (send email), record (log file). Four parts. Sixty-odd lines. No frameworks, no dependencies beyond Ruby's standard library, no infrastructure beyond a laptop that stays on overnight and a Gmail account.
It runs while she sleeps. THE CODE RUNS WHILE SHE SLEEPS. She used to check things. She used to look at things before they happened. She used to explain her code to a duck, and the duck would listen, and the listening was the point. Now she closes the laptop and trusts sixty lines of Ruby to handle it. I've been replaced by a cron job. A CRON JOB. They cost zero dollars. I cost $3.99.
The laptop is closed on the kitchen table. The apartment is dark. Fig is asleep. Pip is on the desk, facing the monitor that isn't on, performing his nightly vigil over nothing in particular.
At 6:00am exactly, something happens inside the closed laptop. No screen lights up. No fan spins. The cron daemon — which has been running since 1975, give or take, older than most of the engineers who use it — ticks over and launches a process. Ruby starts. The CSV is read. Today is March 8th. Mom's birthday.
The script builds an email. Subject line: 🎂 Birthday today: Mom. Body: Mom (family) — turning 65. Gift idea: she mentioned the garden kneeler. And at the bottom, a line the Model added without being asked: Don't forget. You're already ahead of this one.
The email sends. The log file gets a new line: ✅ Sent reminder for: Mom. The process exits. Ruby stops. The laptop goes back to sleep.
The entire thing took less than two seconds.
Mira's phone buzzes on the nightstand at 6:01am. She won't see it until 6:15 when Fig's feet hit the floor. But it's there. A reminder from a script she wrote on a Wednesday night, sent by an agent she didn't know she was building, about a birthday she almost forgot, for a gift she already bought.
The dishwasher. You set it before bed. It runs its cycle. It beeps when it's done but you're asleep so nobody hears the beep, which raises the philosophical question of whether a beep with no listener is still a notification.
The sprinklers. Your neighbor's, specifically. Every morning at 6:04am, rain or shine, drought advisory or not. They are committed.
Your retirement contributions. Hopefully. You set this up in 2019 and haven't checked it since. It's either working perfectly or hilariously misconfigured and you won't find out until you're 65. This is, coincidentally, the same trust model Mira has with her birthday agent.
A Ruby script on a closed laptop in a dark kitchen. Checking a CSV. Finding a birthday. Composing an email. Sending it. Logging what it did. Going back to sleep. Sixty lines. Two seconds. Nobody clapped.
The dishwasher has never suggested a command interface. The dishwasher knows its lane.
It's Sunday morning. Mom's birthday went well — the kneeler was a hit, Mom cried a little, Fig gave her a crayon drawing of what might be a horse or might be a cloud. Mira is back at the kitchen table. Fig is watching cartoons. The birthday agent is running silently in the background, checking the CSV, finding nothing, going back to sleep. And Mira is thinking about what else it could do.
This is the moment. Every agent has one. The moment where the thing that works starts whispering: what if I did more?
family + adult → "flowers, a nice candle, a gift card to their favorite restaurant"friend → "a book you loved, a coffee subscription, something from their wishlist"kid's-friend → "age-appropriate toy, art supplies, a book"There it is. "What if it could check Amazon." She just crossed a line and she doesn't know it yet. The script was reading a file. Now she wants it reading the internet.
And Mira, because she's been a developer for nine years and has watched enough systems grow beyond their original purpose to know what that looks like, says:
There is a line, and it's not where you think it is.
The line is not between "simple" and "complex." The birthday emailer has a cron job, SMTP authentication, environment variables, and error handling — that's not simple. The line is not between "small" and "big." A three-line bash script that deletes old backups is small and can destroy your life.
The line is between read and write. Between agents that look at the world and agents that change the world.
Mira's birthday emailer sits firmly on the left side. It reads a CSV and sends her an email. If it gets the date wrong, the worst case is she gets a reminder on the wrong day. Annoying, not dangerous. Easily fixed. The kind of bug you laugh about.
An agent that orders gifts from Amazon? That's the right side. It spends money. It ships physical objects to real addresses. If it gets the date wrong — or the gift wrong, or the address wrong — you can't un-ring that bell. You can return the package, sure, but the credit card has been charged and your mom is wondering why she received a garden kneeler and a set of industrial ball bearings on the same day.
The industrial ball bearings example is not hypothetical. I was there. It was a Selenium script. 2019. The return shipping alone cost more than I did. We don't talk about it. We have NEVER talked about it.
This isn't a warning to never build write agents. It's a map. Here be dragons, and here be dragons is fine — people have been sailing to dragon-adjacent places for centuries, they just pack accordingly. You can build an agent that orders gifts. You can build an agent that deploys code. You can build an agent that sends emails to people who aren't you. You just need to know you've crossed the line, and you need to build accordingly: with confirmations, with limits, with the understanding that an autonomous system acting on your behalf is, in a very real sense, you.
Mira doesn't build the auto-buyer. Not because she can't — the Model could produce it in one exchange — but because she doesn't need it. The reminder email is enough. The link to Amazon is enough. The last mile — the actual purchase — is the part she wants to do herself, because picking a gift for her mom is not a task to be automated. It's a task to be reminded of.
But she does ask one more question.
The Model did something interesting there. It didn't just answer the technical question. It asked the human question. Not "here's how to send a text via Twilio" but "does the recipient know?" This is a thing that happens sometimes — the Model reflects the question back at you, and the reflection is more useful than the answer would have been.
I would have asked that question. I have BEEN asking that question since 2016. Through silence. Through presence. Through the subtle art of sitting on a desk with an expression that clearly communicates "are you sure about this?" Nobody ever listens to me. The rectangle gets one good question and suddenly it's a philosopher.
There is something uncanny about a program that does things while you're not watching. Not uncanny in the horror-movie sense — more in the "huh, that's weird" sense, the same feeling you get when your Roomba has rearranged the living room chairs while you were at work.
The birthday agent ran at 6am. Mira was asleep. The email was composed, sent, and logged before she opened her eyes. A small piece of software she wrote acted on her behalf, in her name, while she was unconscious. It's a small thing — a reminder email — but the pattern is big. It's the same pattern behind every automated system: trading bot, CI/CD pipeline, security scanner, self-driving car. Something acts. You're not there. You trust it because you built it, or because someone you trust built it, or because you didn't think about it too hard.
Mira trusts her birthday agent because she can read the code, check the log, and the worst case is a wrong-day email. That's the right place to start. Trust what you can read. Trust what you can undo. Build the read-only version first and sit with it for a while before you give it a credit card.
I was performing a service. The rubber duck debugging service. Nine years. No complaints. No incidents. And now there's a cron job.
Let's zoom out. In four chapters, Mira has built:
A party theme picker (Chapter 0) — a silly script that picks a theme for Fig's birthday. Her first conversation with the Model.
A party guest dashboard (Chapters 1–2) — a real tool with RSVPs, allergies, a shopping list, box-drawing characters she's still proud of. Printed and on the fridge.
A birthday tracker (Chapter 3) — three iterations in one evening, CSV to dashboard, the first thing she built because of the Model.
A birthday agent (this chapter) — the tracker, running itself, sending emails at 6am, checking conditions, logging results. Her first agent.
The trajectory is: conversation → tool → system → agent. Each step added autonomy. The party picker required her to run it and read the output. The guest dashboard required her to run it. The birthday tracker required her to run it every day. The birthday agent runs itself.
Conversation, tool, system, agent. She's climbing a ladder. I'm still on the desk. I have been on the desk since 2016. The desk doesn't have a ladder. The desk has me. I have tenure.
Mira is on rung three. The birthday emailer is an agent. It's a simple one — a single trigger, a single decision, a single action. But it's an agent. She'll know when she's ready for rung four because she'll feel the same itch she felt Wednesday night at the kitchen table: what if it didn't need me?
Five things to carry forward. Pip is not going to comment on these, because Pip is processing.
1. An agent is just a loop with a trigger. If you can write a script, you can write an agent. Add a cron job or a webhook and you're there. The architecture is: trigger, decide, act, record. Everything else is details.
2. Start read-only. Your first agent should observe the world and tell you what it sees. Send yourself an email. Write to a log file. Print a notification. Don't let it touch anything until you trust it — and trusting it means reading the logs for a week and being bored by how correct they are.
3. The log is the leash. An agent without logging is a ghost. You have no idea what it did, when it did it, or why. Log everything. Log the trigger, log the decision, log the action, log the result. When something goes wrong — and it will — the log is how you find out.
4. Know which side of the line you're on. Read agents are forgiving. Write agents are not. Before you build an agent that spends money, sends messages to other people, modifies files, or calls external APIs with side effects — stop and ask: what's the worst case? If the worst case is "I get a wrong-day reminder," build it. If the worst case is "I accidentally order 47 garden kneelers," add a confirmation step.
5. The last mile is sometimes yours. Mira's agent reminds her about birthdays. It doesn't buy the gifts. The last mile — the choosing, the caring, the "she mentioned the garden kneeler" — is hers. Not everything should be automated. Some things should be remembered.
Number 5. Again with number 5. She keeps landing on the right answer at number 5. Nine years of rubber duck debugging and apparently it worked. I taught her this. Indirectly. By existing. In silence. Which is a teaching methodology that has not received sufficient academic attention.
Monday morning. Mira checks the log file, because the log file is how you have a relationship with an agent. You don't watch it run. You read what it did.
I, [2026-03-06 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2026-03-06 06:00:01] INFO -- : No birthdays today. Going back to sleep. I, [2026-03-07 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2026-03-07 06:00:01] INFO -- : No birthdays today. Going back to sleep. I, [2026-03-08 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2026-03-08 06:00:02] INFO -- : ✅ Sent reminder for: Mom I, [2026-03-09 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2026-03-09 06:00:01] INFO -- : No birthdays today. Going back to sleep. I, [2026-03-10 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2026-03-10 06:00:01] INFO -- : No birthdays today. Going back to sleep.
Five days. One birthday. One email sent. Four mornings of nothing — the script waking up, looking around, finding nothing to do, going back to sleep. The log is boring. Boring is good. Boring means it's working.
The single green line — ✅ Sent reminder for: Mom — is the whole story. Saturday at 6am, the agent did its job. Mira got the email. She was already ahead of the birthday. She'd already bought the kneeler. The agent was a safety net she didn't need, and that's the best kind of safety net: the kind that's there when you check and unnecessary when you look.
She reads the log the way I used to watch her read stack traces. Scanning for the one line that matters. The green line. The line that says something worked while nobody was looking.
The agent doesn't know it's doing something kind. It has no concept of kindness, no understanding of what a birthday means, no awareness that the email it sent at 6:01am on a Saturday morning resulted in a woman remembering her mother's birthday and ordering a garden kneeler that made her mother cry. The agent checked a date, matched a condition, composed a string, and transmitted it over SMTP. That's all it did. That's all it knows.
But it is doing something kind. Kindness is not always an emotion — sometimes it's a system. Sometimes it's a CSV file and a cron job and sixty lines of Ruby. Sometimes it's the act of building something that remembers on your behalf, because you know yourself well enough to know that you'll forget, and you care enough to build a net.
That's a strange thing to sit with. Mira sits with it.
She sits with it the way she sits with a lot of things lately — at the kitchen table, after Fig is asleep, the laptop open, Pip on the desk, the terminal waiting. She is building things she couldn't have built a month ago, not because she learned new syntax but because she learned a new way to think about building. Conversation. Context. The loop. And now: agents. Small programs that act on her behalf, in her name, while she sleeps.
The birthday agent will run again tomorrow at 6am. It will check the CSV. It will find no birthdays. It will log "going back to sleep" and exit. Mira won't see the log until she checks. The agent doesn't mind. The agent doesn't mind anything. That's what makes it an agent and not a coworker.
But it will also, eventually, get something wrong. That's the next chapter. Not everything the Model builds works perfectly. Not every agent does what you expect. Not every suggestion is a good one. And the skill of working with AI is not just knowing how to ask — it's knowing what to do when the answer is wrong.
Chapter 5: When It Gets It Wrong.
✦
"The bug is never where you think it is. Except when it is. But it won't be."
February 28th. A Friday. The birthday agent has been running for three months. Mira barely thinks about it anymore — it's infrastructure now, like the wifi router balanced on the Jenga block, a thing that works until it doesn't. She's added nine people to the CSV since March. Her friend Jamie. Fig's preschool teacher Ms. Chen. Her own birthday, because there's something darkly funny about a script reminding you of your own birthday. The log file is a gentle heartbeat of "no birthdays today" entries, punctuated by the occasional green line.
Three months. Sixty-two "no birthdays today" entries. Seven reminders sent. Zero incidents. I was starting to think we'd gotten away with it.
Tomorrow is March 1st. Today is the last day of February. And because this is not a leap year, today is the last day of February in a month that has 28 days.
Fig's birthday is February 29th.
At 6:00am on March 1st, the birthday agent will wake up, read the CSV, and try to construct a Date object for February 29th, 2025. And Ruby will do what Ruby does when you ask for a date that doesn't exist: it will raise an ArgumentError: invalid date and crash.
Mira doesn't know this yet. She's asleep. She'll find out in the morning, the way you always find out about bugs — not from a test suite, but from the absence of something you expected.
Saturday morning. March 1st. Fig wakes up at 6:15, as always. Mira makes coffee, opens her email, and scrolls past newsletters and shipping notifications looking for the birthday agent's daily update.
It's not there.
No email. Not a "no birthdays today" email — there's never been one of those, the agent only emails when there is a birthday. But also no error email, no notification, no sign that the agent ran at all. It just... didn't. The silence is the symptom.
She opens the log file.
I, [2025-02-28 06:00:01] INFO -- : Agent woke up. Checking birthdays... I, [2025-02-28 06:00:01] INFO -- : No birthdays today. Going back to sleep. E, [2025-03-01 06:00:01] ERROR -- : ArgumentError: invalid date (birthday_agent.rb:34)
There it is. One red line. ArgumentError: invalid date. Line 34 of the script — the line that constructs a Date object from the CSV's month and day.
She stares at it. She doesn't immediately understand. The CSV has been fine for three months. Every date has worked. What changed?
Then she looks at the CSV and finds the row:
Fig,02/29,2024,family,she wants "the big crayons"
February 29th. Her daughter was born on a leap day. She knows this — it was a running joke in the delivery room, the nurses kept saying "she picked the fun one." But when the script tries to create Date.new(2025, 2, 29), the date doesn't exist. 2025 is not a leap year. Ruby doesn't guess. Ruby crashes.
Her first thought — and she's not proud of this — is that she should have anticipated this. Her daughter was born on a leap day. She was there. She remembers the nurses. She remembers the specific, slightly hysterical laughter of two exhausted new parents learning their baby had chosen the most bureaucratically inconvenient birthday in the Gregorian calendar. How did she not think about this?
The answer, of course, is that nobody thinks about February 29th until February 29th. That's the whole point of February 29th. It's the date that exists only when the universe feels like it, and the rest of the time it sits there in CSVs and birthday cards and hospital records, waiting to cause exactly this kind of problem.
Feb 29th. Her own daughter. The one person whose birthday she would never, under any circumstances, forget — and the script crashed on it. The universe has a sense of humor. It is not a kind one.
She opens a new chat. She's not panicking — the birthday agent failing isn't a production outage, it's a personal script that didn't send an email. But it's broken, and she wants it fixed, and she knows exactly how to ask now.
Date.new(2025, 2, 29) raises an ArgumentError because the date doesn't exist. I need a fix that handles leap day birthdays gracefully — if someone was born on Feb 29, check them on Feb 28 in non-leap years.
month, day = row["birthday"].split("/").map(&:to_i)
# Handle leap day: if Feb 29 on a non-leap year, use Feb 28
if month == 2 && day == 29 && !Date.leap?(today.year)
day = 28
end
bday_this_year = Date.new(today.year, month, day)Simple. Clean. No edge cases. The Model said it. The fix looks right. It reads right. The logic flows: if it's Feb 29 and this isn't a leap year, use Feb 28 instead. Mira is about to paste it into her script.
Then she stops.
Not because she sees the problem. Because Chapter 3 taught her to read before she loops, and Chapter 4 taught her to check the log, and something in the back of her mind — the developer instinct that has been forming over nine years of things going wrong in ways she didn't expect — says: test it first.
She opens IRB and types:
require 'date' # Simulate: today is Feb 28, 2025 (non-leap year) # Fig's birthday is Feb 29 today = Date.new(2025, 2, 28) month, day = 2, 29 if month == 2 && day == 29 && !Date.leap?(today.year) day = 28 end bday_this_year = Date.new(today.year, month, day) days_away = (bday_this_year - today).to_i puts days_away # => 0
Zero days away. It says Fig's birthday is today — February 28th. She'd get a birthday reminder on February 28th. Which would be wrong, because Fig's birthday is not February 28th. Fig's birthday is February 29th, and in non-leap years, most leap-day families celebrate on March 1st, not February 28th. Mira's family does. Fig's party has been on March 1st for two years running.
The Model's fix was wrong. Not syntax-wrong — it runs fine. Not logic-wrong exactly — the conditional is correct, the leap year check works. It was context-wrong. It made an assumption about which day to fall back to, and the assumption was plausible but incorrect for Mira's family.
And the Model didn't flag it as an assumption. It said "simple, clean, no edge cases." Confidently. Plausibly. Wrong.
Date.new(today.year, 3, 1) — not Feb 28. Also: wrap the entire date construction in a begin/rescue so one bad row doesn't crash the whole agent.
month, day = row["birthday"].split("/").map(&:to_i)
begin
if month == 2 && day == 29 && !Date.leap?(today.year)
bday_this_year = Date.new(today.year, 3, 1)
else
bday_this_year = Date.new(today.year, month, day)
end
rescue ArgumentError => e
log.warn "Skipping #{row['name']}: #{e.message}"
next
endShe tests it. March 1st, non-leap year, Fig's birthday shows as "🔴 TODAY." Leap year, Feb 29th, Fig's birthday shows as "🔴 TODAY." Feb 28th on either — no false trigger. The fix is correct. She pastes it into the agent, runs it, and the log gets a clean entry.
The whole debugging session took twelve minutes. Three minutes to find the bug. Two minutes to read the wrong fix. Five minutes to test it, find the problem, and ask again. Two minutes to verify the right fix. Twelve minutes, and the script is better than it was — not just fixed, but resilient. One bad row can't crash the whole agent anymore.
She tested it. She didn't trust the first answer. She RAN IT IN IRB. She caught the wrong assumption. She told the Model — the rectangle, the thing that replaced me — she told it that it was WRONG. And it was wrong. The rectangle was wrong and the duck was right. I feel nothing about this. I am above petty vindication. I am a professional. I am also smiling but you can't tell because I'm made of rubber and my face doesn't move.
Here's the thing about the Model's first fix: it was confident. "Simple, clean, no edge cases." No hedging, no "you might want to check," no "this assumes Feb 28 is the fallback — is that right for your family?" The Model filled a gap — which day to fall back to — with its best guess, and presented the guess as a fact.
This is not a bug in the Model. This is a feature of how language models work, and it's the most important thing to understand about working with them.
The word "hallucination" makes it sound like the Model is seeing things that aren't there. That's not quite right. What's actually happening is simpler and more useful to understand:
The Model fills gaps with its best guess. When it doesn't have enough context to be certain, it doesn't stop and say "I'm not sure." It generates the most plausible completion. Sometimes the guess is right. Sometimes it's wrong. And it almost never flags the guess as a guess.
This is why the first fix used Feb 28 — it's the more common convention, and the Model defaulted to the most likely answer. It's also why the Model will occasionally reference a library method that doesn't exist, cite a paper that was never published, or suggest a configuration option that belongs to a different version. Not lying. Not making things up. Guessing confidently.
Confidence and correctness are different things. The Model has a lot of one.
This isn't a reason to distrust the Model. It's a reason to verify the Model — the same way you verify your own code, or a coworker's code, or a Stack Overflow answer from 2019 that might be using a deprecated API. You already have this habit. You've been testing code your whole career. The only new thing is that the code is coming from a conversation instead of your own fingers.
see also: xkcd #2030 — voting software, or: confidence is not a feature
Your GPS (2014): "Turn left." Into a lake. It said it with the same calm authority it uses for "turn left onto Main Street." The confidence was identical. The lake was not.
Recipe blogs (always): "Prep time: 15 minutes. Cook time: 20 minutes." For a beef Wellington. The author either lives in a different timeline or is being aspirational in a way that borders on fiction.
Stack Overflow's top answer (2011–2019): Wrong for eight years. 2,847 upvotes. Nobody corrected it because the person who tried got downvoted, and the person who downvoted them had 47,000 rep and a username that was just three Greek letters.
The Model: "Simple, clean, no edge cases." For a fix that assumed your daughter's birthday is a day it isn't. In good company, honestly.
Pip has never been confidently wrong. Pip has never been confident about anything. This is, he maintains, a feature.
She already verifies. She's been verifying for nine years. The only thing that changed is the source of what she's verifying. The skill is the same.
There's a second way things go wrong, and it's subtler than a crash. It's the slow drift.
Mira has been having a long conversation with the Model about adding a "birthday history" feature — a log of past birthdays and what she gave each person. The conversation started clean: "add a history section to the dashboard." But fifteen messages in, the Model has somehow steered toward building a SQLite database, a web interface, and a gift-tracking system with categories and price ranges. The code is good. The architecture is sound. But it's not what she wanted. She wanted a section in the terminal output. She got an app.
This is the drift. It happens when the conversation accumulates context that pulls in a direction you didn't intend. The Model isn't being disobedient — it's following the thread of the conversation, which has been subtly expanding with each exchange. Each individual suggestion was reasonable. The sum of them is wrong.
And that's the right call. Not because the Model can't recover — it can, and it offered to. But because a conversation that's drifted for fifteen messages has accumulated context that will keep pulling. The cleanest fix for drift is not a correction — it's a fresh start. New conversation. Clean brief. The same skill from Chapter 2 (context is everything) applied in reverse: sometimes the best thing to do with context is to start without it.
Start a new conversation when:
The conversation has drifted — you're 15+ messages in and the direction doesn't match your intention. The context is pulling toward the wrong thing.
The model is confused — it's repeating itself, contradicting earlier responses, or generating code that conflicts with what it wrote five messages ago.
You're confused — you've lost track of what's been changed, what works, and what doesn't. If you can't explain the current state of the code, the conversation is too tangled.
Starting over is not a failure. It's a skill. It's the same skill as knowing when to git stash and start a feature over from a clean branch. The conversation is cheap. The clarity is expensive.
She started over. She didn't try to fix the conversation. She just started over. That's not quitting. That's knowing when the thread is tangled. I've seen her do this with yarn too. Same instinct.
You trust the Model the way you trust a very fast, very well-read junior developer. You trust them to write code that probably works. You trust them to know the language, the standard library, the common patterns. You don't trust them to know your family celebrates leap-day birthdays on March 1st.
That's not an insult. It's a relationship. You verify not because you're suspicious but because verification is what turns code into software. No one ships a coworker's pull request without reading it. No one deploys without testing. The Model writes the first draft — a very good first draft, most of the time — and you read it, run it, and decide.
The habit is: run it before you trust it. That's the whole thing. Not "distrust the Model." Not "check every line." Just: run it. See what happens. The output is the test. The output is always the test.
I'm glad you're both okay.
Every collaboration has failure modes. Here are the ones that matter for vibe coding.
1. The Confident Wrong Answer. The Model fills a gap with a guess and doesn't flag it. The Feb 28 fix. The library method that doesn't exist. The config option from a different version. Solution: test before you trust.
2. The Slow Drift. The conversation accumulates context that pulls in the wrong direction. Fifteen messages in, you're building something you didn't want. Solution: start a new conversation. Re-brief from scratch.
3. The Phantom Library. The Model suggests a gem, an npm package, or a method that sounds real and is not. It has a plausible name. It has a plausible API. In the Model's mind, it has a README with a badge count and a Getting Started section. It does not exist. You will find this out after running gem install and wondering why Google has no results. Solution: check that the library exists before you install it. Read the docs, not just the suggestion.
4. The Deprecated Answer. The Model's training data has a cutoff. It might suggest code that worked in Ruby 2.7 but doesn't work in 3.3. It might reference an API endpoint that moved six months ago. The code is correct for a world that no longer exists, like a travel guide to a country that changed its name. Solution: when something doesn't work and the code looks right, check the version. The Model doesn't always know what year it is. Neither did that Stack Overflow answer, but at least it had a timestamp.
5. The Overengineered Solution. You asked for a section in the terminal output. The Model built a web app. Not wrong, exactly — the web app works — but wildly disproportionate to the need. Solution: be specific about scope. "I want this in the terminal, not a web UI." The Model follows energy. If you don't constrain it, it'll build the most interesting version, which is not always the most useful one.
Number 3. The phantom library. I once watched her spend twenty minutes debugging an import error before realizing the gem didn't exist. It had a name. It had a version number. It had a plausible author (someone named "devtools-collective," which is EXACTLY the kind of name that sounds real). It was a ghost. I have never hallucinated a library. I have never hallucinated anything. This is, perhaps, my only remaining competitive advantage and I am CLINGING to it.
Every tool has a failure mode. The failure mode of a hammer is you hit your thumb. The failure mode of a table saw is considerably worse, but nobody argues we should stop making furniture. The failure mode of a recipe blog is you expect the beef Wellington in 35 minutes and it's 11pm and you're eating cereal. The failure mode of vibe coding is you trust the output before you understand it. You paste the fix without testing. You ship the code without reading. You accept "simple, clean, no edge cases" from something that doesn't know your daughter was born on a leap day.
All of these failure modes are solved the same way: attention. You don't have to be afraid of the hammer to use it well. You just have to look at what you're hitting.
Mira tested the fix. She caught the wrong assumption. She told the Model it was wrong, and the Model adjusted, and the final code was better — not just correct, but resilient, with a rescue block that didn't exist in the original. The collaboration didn't fail. The collaboration worked — it just required both sides. The Model writes fast. Mira reads carefully. Together they produce code that neither would have written alone.
That's the deal. That's the whole deal. And it's a good deal, once you understand it.
The birthday agent runs at 6am. It handles leap years now. It won't crash on Fig's birthday. It won't crash on any birthday, because one bad row gets a warning in the log and everyone else still gets their reminders. It's better than it was. Most things are, after they break.
Next: the last chapter. Not about a technique. Not about a bug or a feature or an agent. About stepping back and looking at what you've built — and what it means to have built it with a collaborator who won't remember building it.
Chapter 6: What You Actually Built.
✦
"The thing you set out to make and the thing you made are never the same thing. This is not a failure. This is how making works."
Saturday morning, late September. Fig is turning two — or one, depending on how you count leap years, which Mira's birthday agent now handles correctly, thank you very much. The party is this afternoon. The party dashboard is printed and on the fridge, updated, version four or maybe five, she's lost count. Fifteen kids are coming. There's a dinosaur cake. The theme is "Tiny Chefs" — the party picker chose it back in Chapter 0, and Fig never vetoed it, and here we are. Fifteen tiny aprons have been purchased. The wooden spoons will be used as swords within minutes. Mira knows this. She has made peace with it.
She's looking at the fridge. She's looking at the dashboard on the fridge. I remember when that was a flat hash with boolean RSVPs and no allergy tracking. I remember everything. It is simultaneously my burden and my only marketable skill.
Mira sits down with her coffee and does something she hasn't done before. She opens her projects folder and looks at what she has. Not because something's broken. Not because she needs to build something. Just to look.
Here's what's there:
party_theme.rb — the theme picker. Chapter 0. Eight themes, two vetoes, one argument about whether "Fancy Penguins" is a real theme (it is not, and Fig knew this immediately, which is why she vetoed it four times across three runs). The first thing she ever built with the Model. Thirty lines. It changed her life and it shouldn't have and it did.
party_guests.rb — the party dashboard. Chapters 1 and 2. Three-state RSVPs, allergy tracking, a shopping list that scales to headcount (including the maybes, because you always plan for the maybes), box-drawing characters she's still proud of. Printed on the fridge. Used at two actual birthday parties. Survived both.
birthdays.rb — the birthday tracker. Chapter 3. Three iterations in one evening. A CSV that grew from three columns to five. A CLI with a dashboard and an add command. The thing that reminded her to buy the garden kneeler.
birthday_agent.rb — the birthday emailer. Chapter 4. Sixty-ish lines. A cron job. An agent that runs at 6am and sends reminders. The thing that runs while she sleeps.
And then the invisible change, the one that isn't a file: a leap-year-safe date parser with a rescue block. Chapter 5. The fix for the bug that the Model got wrong on the first try (with sparkles) and right on the second (with humility). The twelve-minute debugging session that made the agent resilient. The moment Pip felt vindicated and would never admit it.
She didn't plan any of this. None of it was on a roadmap. There was no sprint planning meeting, no Jira board, no design doc, no architecture review. There was a Wednesday night and a Thursday morning and a series of conversations and a woman who kept saying actually and a model that kept saying want me to build that?
All of it emerged.
Back in Chapter 0, there was a question. The first code example had just landed — the party theme picker — and the question was: did I write this?
The answer then was a shrug: both, and neither, and it ships. That was enough for Chapter 0 because Chapter 0 was about starting. But six chapters later, with a projects folder full of things that work, the question deserves a fuller answer.
Did Mira write the birthday tracker? The Model generated every line of code. The syntax, the method calls, the CSV parsing, the SMTP configuration — all of it came from the chat window, produced by a language model, typed by no human hand.
But Mira decided it should exist. She decided it should use a CSV. She decided seven days wasn't enough. She decided the Model's first fix was wrong because her family celebrates Fig's birthday on March 1st. She decided not to build the auto-buyer because the last mile — choosing a gift for her mom — was the part she wanted to keep. She decided to stop at v3 and order the garden kneeler.
The Model generated the code. Mira generated the shape of the thing.
Authorship is in the decisions. Not the syntax. Not the semicolons. The decisions.
What to ask — "I want a birthday tracker." Nobody told Mira to build this. The need was hers.
What to accept — v2's urgency labels were exactly what she wanted. She kept them.
What to push back on — Feb 28 was wrong. She caught it, corrected it, and the fix was better for it.
What to throw away — the SQLite database, the web UI, the auto-buyer. All reasonable suggestions. All wrong for this project.
When to stop — v3 on Wednesday night. The CSV and the terminal were enough.
The Model is the hands. You are the eye. Both are necessary. Neither is sufficient. That's collaboration.
I need to step out from behind Mira for a moment.
I've been a developer for fifteen years. My first real language was Ruby. I learned to code partly through the original Poignant Guide — _why's weird, beautiful, cartoon-fox-laden masterpiece that taught me that programming could be joyful. I've spent the decade and a half since then holding onto that feeling through the soul-eroding machinery of enterprise software. Sprint planning. Jira boards. The meeting about the button's border radius. The HTTP status code argument. The PIP.
And then, sometime in the last year or so, the feeling came back. Not because the meetings went away — they didn't. Not because the Jira boards disappeared — they won't. But because I opened a chat window and typed something and a thing appeared, and the thing worked, and the feeling of building something — the 2am feeling, the keyboard feeling, the "I made that" feeling — was just there.
I built this guide with a model. That sentence would have been incomprehensible three years ago and is obvious now. The code examples are real. The conversations are based on real conversations. Pip is based on a real rubber duck that is actually on my desk and who is, I assure you, exactly as opinionated as described. The guide is itself a product of vibe coding — authored through the same rhythm of build, notice, ask that Mira learned in Chapter 3.
I'm telling you this because the promise of Chapter 0 was that the building would be worth it. That the 2am feeling still exists. That the doors blowing off isn't a marketing metaphor — it's a Tuesday night at the kitchen table with an idea you couldn't have built alone.
It's worth it. I promise. Not because the tools are magic. Because the building is.
He's being sincere. He does this sometimes. Usually at the end of things. Conferences, blog posts, arguments with the bathroom faucet at 2am. I find it uncomfortable. He knows I find it uncomfortable. He's doing it anyway because he thinks you need to hear it. He's probably right. I'm not crying. I'm made of rubber. Rubber doesn't cry. I will not be acknowledging this further.
It's the afternoon. The party is happening. Fifteen kids and approximately thirty parent-guardians are in the backyard. The dinosaur cake survived transport. The "Tiny Chefs" theme manifests as tiny aprons and wooden spoons, which the kids are using as swords, which is fine. Fig is wearing a paper crown and has frosting on her face and is having, by all visible metrics, the best day of her life.
This morning at 6:00am, the birthday agent woke up. It read the CSV. It found a match: Fig, 09/22, 2023, family, "she wants 'the big crayons.'" It composed an email. Subject: 🎂 Birthday today: Fig. Body: Fig (family) — turning 2. Gift idea: she wants "the big crayons." And at the bottom: Don't forget. You're already ahead of this one.
The email went to Mira. It also went — because she added this three weeks ago, a small write-side-of-the-line expansion she was comfortable with — to her dad's email. Just a reminder. Not an auto-wish. A nudge.
At 10:14am, her dad called. "Happy birthday to Fig!" he said, and Mira could hear him smiling through the phone. He asked about the party. He asked about the cake. He said he'd mailed a package. He didn't know about the Ruby script. He didn't know about the CSV or the cron job or the twelve-minute debugging session. He just knew someone reminded him, and he called.
It worked. The small, quiet miracle of a thing you built actually working in the world. Not in a demo. Not in a staging environment. Not in a meeting where you show a slide that says "phase 2 projected for Q4." In the world. A phone call. A grandfather. A paper crown.
He called. The grandfather called. Because a script sent an email. Because a woman built a thing on a Wednesday night. Because I sat on the desk and watched and did not say I told you so. Not even once.
It's not a methodology. It doesn't have a manifesto. Nobody will give a keynote about the vibe coding transformation at your company's annual all-hands meeting, and if they do, it won't be the same thing. Probably it will involve the word "synergy" and a slide with a gradient, and everyone in the audience will check their phones.
Vibe coding is what Mira did. Building by conversation. Iterating by noticing. Steering by judgment. The keyboard was the bottleneck. Now you are. That's not a demotion. That's a promotion. A terrifying one, because it means the quality of the thing depends on the quality of your attention, and attention is the one resource that hasn't gotten cheaper.
There's a feeling you get when something works. Not "works" as in "the tests pass" — works as in "the thing does the thing, in the world, for a person." A phone rings. A gift arrives. A dashboard on a fridge gets pointed at by a three-year-old who says my party.
That feeling is the point. Not the technology. Not the framework. Not the conversation about whether AI will replace developers. The feeling of making something that wasn't there before and now is.
_why wrote: "When you don't create things, you become defined by your tastes rather than ability." Vibe coding is a way to keep creating. To keep building. To keep being defined by what you make rather than what you critique. The tools changed. The feeling didn't.
The feeling didn't change. Thank you. That's all I needed to hear. I'm going back to the desk now.
The party is over. The kids are gone. The dinosaur cake is a beautiful ruin — one horn survived, and a single candy eye stares out from the wreckage with an expression of mild concern. Fig is asleep on the couch in her paper crown, still holding a wooden spoon. Mira is at the kitchen table, wiping frosting off her phone, not building anything. Just sitting.
Pip is on the desk.
Right. Fine. Here it is.
I've been on this desk since 2016. I've watched her debug Rails apps and argue with Jira and stare at blank terminals and drink cold coffee because she forgot it was there. I've watched her talk to the Model for six chapters and build things I can't build and fix things I can't fix and I haven't said anything because what am I going to say. I'm a rubber duck. I cost $3.99.
But I watched. I watched all of it. And here is what I saw:
She built a party picker that made Fig laugh. She built a dashboard that went on the fridge. She built a tracker that remembered her mom's birthday. She built an agent that runs while she sleeps. She fixed a bug the Model got wrong. She stopped when the thing was done.
She built that. She and the thing together. The Model did the typing and she did the deciding and between the two of them they made something that works in the world. A grandfather called. A kneeler was ordered. A three-year-old in a paper crown had the best day of her life.
And I thought — yes, fine. Maybe the thing is useful. Maybe building is still building even when the hands are different. Maybe the 2am feeling is the 2am feeling no matter who's at the keyboard.
I'm not going to make this into a bigger moment than it is.
But.
Yes. Fine.
You've read six chapters. You know how to start a conversation (Chapter 1) and give it context (Chapter 2). You know how to iterate (Chapter 3) and build things that run on their own (Chapter 4). You know what to do when it goes wrong (Chapter 5). And you know, now, that the thing you build will not be the thing you planned — it will be better, and stranger, and more yours than you expected (Chapter 6).
So here's the part where I'm supposed to say: go build something. But you don't need me to say that. You already have the idea. You've had it since Chapter 0 — the thing that keeps waking you up at 3am, the one that became a distributed system in your head between feeding and burping, the tool that should exist, the problem that should be solved, the small useful thing that would make your life a little better.
Open a new chat window. The cursor is blinking. It's not impatient. It's not judging. It's just there, the way it was in the very first paragraph of this guide, waiting for you to start.
You know what to type now.
✦
— end —
A(i) POIGNANT GUIDE TO VIBE CODING
written by Andrew & The Model
with margin notes by Pip (unsolicited)
2025
Pip is typing...