<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Spec-Driven-Development on Tiziano Basile | Software Engineer</title><link>https://bstz.it/tags/spec-driven-development/</link><description>Recent content in Spec-Driven-Development on Tiziano Basile | Software Engineer</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Tiziano Basile</copyright><lastBuildDate>Thu, 06 Aug 2026 09:00:00 +0000</lastBuildDate><atom:link href="https://bstz.it/tags/spec-driven-development/index.xml" rel="self" type="application/rss+xml"/><item><title>A BMAD workflow, end to end: shipping a feature the spec-driven way</title><link>https://bstz.it/p/a-bmad-workflow-end-to-end-shipping-a-feature-the-spec-driven-way/</link><pubDate>Thu, 06 Aug 2026 09:00:00 +0000</pubDate><guid>https://bstz.it/p/a-bmad-workflow-end-to-end-shipping-a-feature-the-spec-driven-way/</guid><description>&lt;p&gt;If the only way you&amp;rsquo;ve ever worked with an AI is to &lt;em&gt;vibe code&lt;/em&gt; with it, which is to say you open the editor, type a prompt, and keep whatever looks roughly right, then this is the post I wish someone had handed me back when I was doing exactly that. It isn&amp;rsquo;t another argument about why spec-driven development is better, because I already made that case &lt;a class="link" href="https://bstz.it/p/dont-be-mad-bmad-instead/" &gt;here&lt;/a&gt;; it&amp;rsquo;s simply one complete pass through a real workflow, with the actual commands, so that you can see for yourself what the whole thing looks like from the inside.&lt;/p&gt;
&lt;p&gt;Together we&amp;rsquo;ll build one small feature with &lt;a class="link" href="https://github.com/bmad-code-org/BMAD-METHOD" target="_blank" rel="noopener"
&gt;BMAD&lt;/a&gt;, taking it all the way from a vague idea to reviewed code, and although the example is deliberately thin it is also complete, running the full sequence of brainstorm → spec → architecture → story → implementation → review without waving our hands over any of the parts that usually get skipped, so that by the end you can see exactly where the friction lives and what it buys you in return.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve never met the BMAD agents before, the &lt;a class="link" href="https://bstz.it/p/bmad-meet-the-crew/" &gt;previous post&lt;/a&gt; introduces the crew, and while you don&amp;rsquo;t strictly need it in order to follow along here, it does help to know that &amp;ldquo;John&amp;rdquo; is a product manager and &amp;ldquo;Amelia&amp;rdquo; is a developer rather than people I actually work with.&lt;/p&gt;
&lt;p&gt;The example is a URL shortener where links can expire. All the artifacts you&amp;rsquo;ll see below are illustrative snippets from that walkthrough.&lt;/p&gt;
&lt;h2 id="the-feature-and-the-trap"&gt;The feature (and the trap)
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the feature: &lt;strong&gt;a URL shortener where links can expire.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now watch how a vibe-coding session usually starts, because you&amp;rsquo;d open a chat and type something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build me a URL shortener in [your stack]. Links should be able to expire.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That prompt &lt;em&gt;feels&lt;/em&gt; complete, but it really isn&amp;rsquo;t, because it&amp;rsquo;s a specification riddled with holes, and the AI is going to quietly fill every one of those holes with a guess that you never actually got to see it make, so it&amp;rsquo;s worth counting them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When does a link expire?&lt;/strong&gt; After a fixed TTL? At an absolute timestamp you pass in? After N clicks? &amp;ldquo;Expire&amp;rdquo; is three different features.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What happens when someone hits an expired link?&lt;/strong&gt; A &lt;code&gt;404&lt;/code&gt;? A &lt;code&gt;410 Gone&lt;/code&gt;? A redirect to a &amp;ldquo;this link expired&amp;rdquo; page? These are &lt;em&gt;observably different&lt;/em&gt; behaviors, and a caller integrating with you will build against whichever one you pick.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do we count clicks?&lt;/strong&gt; If expiry can be click-based, you need a counter. If it can&amp;rsquo;t, you probably still want analytics. That&amp;rsquo;s a storage decision hiding inside a one-line prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&amp;rsquo;s the ID scheme?&lt;/strong&gt; Random short codes? Collisions? Custom aliases?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vibe coding doesn&amp;rsquo;t make any of these decisions go away, it simply arranges for &lt;em&gt;someone else&lt;/em&gt; to make them silently and invisibly at generation time, and then it hands you back a pile of code that you now have to reverse-engineer in order to discover what was actually decided on your behalf. So when the &amp;ldquo;expired link returns 404&amp;rdquo; bug report lands on your desk three weeks later, you&amp;rsquo;ll find yourself debugging a decision that nobody ever consciously made in the first place.&lt;/p&gt;
&lt;p&gt;Spec-driven development, at its core, is really just this: you &lt;strong&gt;make the decisions before you write the code, on purpose, and in a place where you can actually see them&lt;/strong&gt;, and BMAD is simply one way of running that process with the AI working inside the loop with you rather than against you.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s run it.&lt;/p&gt;
&lt;h2 id="the-mental-model-in-30-seconds"&gt;The mental model, in 30 seconds
&lt;/h2&gt;&lt;p&gt;Two things to hold in your head before we start:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;BMAD is a team of agents rather than a single assistant.&lt;/strong&gt; Each one is a persona with a specific job to do, so there&amp;rsquo;s an analyst who interrogates the idea, a product manager who writes the requirements, an architect who makes the technical calls and a developer who implements them, and you move between these personas deliberately as the work progresses. In the commands that follow, every phase can be run either by summoning the persona directly with &lt;code&gt;/bmad-agent-*&lt;/code&gt; or by invoking its underlying workflow with &lt;code&gt;/bmad-*&lt;/code&gt;, and you should feel free to use whichever of the two you prefer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The real output of the early phases is not code at all, it&amp;rsquo;s artifacts.&lt;/strong&gt; These are things like a brief, a PRD, an architecture document and a story file, all of them plain markdown that lives right there in your repository, and the code itself only arrives &lt;em&gt;last&lt;/em&gt;, sitting downstream of a set of documents that you can read, review and version like anything else.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Broadly speaking there are two stages to all of this, a &lt;strong&gt;planning&lt;/strong&gt; stage in which you work out what to build and how to build it, followed by an &lt;strong&gt;implementation&lt;/strong&gt; stage in which you actually build it, and the whole point of the arrangement is that planning leaves behind durable, reviewable artifacts which then carry all of their context forward into implementation, so that the AI writing your code is working from a genuine spec rather than from a vibe.&lt;/p&gt;
&lt;figure class="mermaid-figure"&gt;
&lt;pre class="mermaid"&gt;flowchart LR
idea([Vague idea]) --&amp;gt; A
subgraph Planning
A[&amp;#34;🔍 Analyst (Mary)&amp;lt;br/&amp;gt;brainstorm&amp;#34;] --&amp;gt; P[&amp;#34;📋 PM (John)&amp;lt;br/&amp;gt;PRD / spec&amp;#34;]
P --&amp;gt; W[&amp;#34;🏛️ Architect (Winston)&amp;lt;br/&amp;gt;architecture&amp;#34;]
W --&amp;gt; S[&amp;#34;📝 Story&amp;lt;br/&amp;gt;context handoff&amp;#34;]
end
subgraph Implementation
S --&amp;gt; D[&amp;#34;💻 Dev (Amelia)&amp;lt;br/&amp;gt;implement&amp;#34;]
D --&amp;gt; R[&amp;#34;🔎 Code review&amp;#34;]
end
R --&amp;gt; code([Reviewed code])
A -.-&amp;gt; brief[/brief.md/]
P -.-&amp;gt; prd[/prd.md/]
W -.-&amp;gt; arch[/architecture.md/]
S -.-&amp;gt; story[/story.md/]&lt;/pre&gt;
&lt;figcaption class="mermaid-caption"&gt;🔍 click the diagram to enlarge&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Everything drawn with a dotted line there is a file that ends up in your repository, and that is precisely the part of the process that vibe coding never produces.&lt;/p&gt;
&lt;h2 id="phase-1-brainstorm-drag-the-hidden-decisions-into-the-light"&gt;Phase 1, Brainstorm: drag the hidden decisions into the light
&lt;/h2&gt;&lt;p&gt;We start with Mary, the analyst, and a brainstorming session:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-agent-analyst # then pick the brainstorming capability
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or go straight to it:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-brainstorming
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The move here is genuinely counterintuitive if you&amp;rsquo;re accustomed to prompting an AI for output, because &lt;strong&gt;you&amp;rsquo;re not asking it for an answer at all, you&amp;rsquo;re asking it to interrogate you&lt;/strong&gt;, and Mary&amp;rsquo;s entire job in this session is to ask you the questions that you quietly skipped over. This is exactly the moment where those four hidden decisions from the trap section earlier stop being invisible and start being things you have to answer for.&lt;/p&gt;
&lt;p&gt;A brainstorm on &amp;ldquo;URL shortener with expiring links&amp;rdquo; should surface, at minimum: which &lt;em&gt;kinds&lt;/em&gt; of expiry we actually want (TTL vs absolute date vs click-count), the expired-link behavior as a deliberate choice, whether click tracking is a feature or just a mechanism, and the ID/alias scheme.&lt;/p&gt;
&lt;p&gt;The artifact that comes out of this phase is a &lt;strong&gt;brief&lt;/strong&gt;, and this really is the part worth staring at for a moment, because it doesn&amp;rsquo;t merely list the things we intend to build; it also records the things we have deliberately decided &lt;em&gt;not&lt;/em&gt; to build, so that those decisions leave a paper trail behind them instead of quietly evaporating. Here&amp;rsquo;s the heart of an example &lt;code&gt;brief.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Decisions (made on purpose, for v1)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Expiry model: absolute timestamp.** A link carries an optional &lt;span class="sb"&gt;`expires_at`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (UTC instant). No &lt;span class="sb"&gt;`expires_at`&lt;/span&gt; means the link never expires.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Expiry is inclusive.** At exactly &lt;span class="sb"&gt;`expires_at`&lt;/span&gt;, the link is already expired.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Behavior on an expired link: &lt;span class="sb"&gt;`410 Gone`&lt;/span&gt;,** with body &lt;span class="sb"&gt;`{ &amp;#34;reason&amp;#34;: &amp;#34;expired&amp;#34; }`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Not a &lt;span class="sb"&gt;`404`&lt;/span&gt;, not a &lt;span class="sb"&gt;`302`&lt;/span&gt; to a landing page.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **ID scheme: 7-character base62 random code,** with collision-retry.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Explicitly out of scope for v1 (deferred, not forgotten)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Relative / TTL expiry (&amp;#34;expire 24h after creation&amp;#34;).
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Click-count expiry, click tracking / analytics.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Custom aliases, custom &amp;#34;this link expired&amp;#34; landing pages.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Take a good look at that &amp;ldquo;out of scope&amp;rdquo; list, because every single line in it is a decision that a vibe-coding prompt would have made &lt;em&gt;for&lt;/em&gt; you, silently and without asking, whereas here every one of them is on the record, deferred deliberately rather than forgotten by accident.&lt;/p&gt;
&lt;p&gt;If you want a more formal artifact than a brainstorm dump:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-product-brief
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Notice what has already happened here, because we haven&amp;rsquo;t yet written a single line of code and we have already caught three or four decisions that vibe coding would otherwise have made for us essentially at random.&lt;/p&gt;
&lt;h2 id="phase-2-prd-turn-decisions-into-requirements-you-can-check"&gt;Phase 2, PRD: turn decisions into requirements you can check
&lt;/h2&gt;&lt;p&gt;Now we switch to John, the product manager, to produce a Product Requirements Document.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-agent-pm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or the workflow directly:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-prd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;John&amp;rsquo;s job is to take something like &amp;ldquo;we decided that links expire by absolute timestamp and that expired links return &lt;code&gt;410 Gone&lt;/code&gt;&amp;rdquo; and convert it into requirements that are precise enough that you could hand them to a complete stranger and expect the right thing to come back, and this is the point at which the fuzzy brainstorm finally hardens into something resembling a contract.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example FR-3 from &lt;code&gt;prd.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### FR-3: Accessing an expired link
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Given** a short link whose &lt;span class="sb"&gt;`expires_at`&lt;/span&gt; is at or before now
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **When** a client requests &lt;span class="sb"&gt;`GET /{code}`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Then** the service responds &lt;span class="sb"&gt;`410 Gone`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **And** the response body includes a machine-readable &lt;span class="sb"&gt;`reason: &amp;#34;expired&amp;#34;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **And** no redirect (&lt;span class="sb"&gt;`3xx`&lt;/span&gt;) is issued
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Non-goals for v1: custom &amp;#34;expired&amp;#34; landing pages, per-link grace periods.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Read that through and notice what has changed, because the &lt;code&gt;410&lt;/code&gt; decision is now &lt;strong&gt;written down, reviewable and testable&lt;/strong&gt;, which means that a month from now the question &amp;ldquo;why does it return 410?&amp;rdquo; has an actual answer with a paper trail attached to it, and that really is the whole difference between a decision and an accident.&lt;/p&gt;
&lt;p&gt;If you want the requirements distilled into a tighter, machine-oriented contract for the downstream agents, BMAD has a dedicated step:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="phase-3-architecture-the-technical-calls-made-once"&gt;Phase 3, Architecture: the technical calls, made once
&lt;/h2&gt;&lt;p&gt;Winston, the architect, takes the &lt;em&gt;what&lt;/em&gt; that we&amp;rsquo;ve established so far and decides the &lt;em&gt;how&lt;/em&gt;, and for a feature this small the right instinct is to keep the whole thing thin, because you really don&amp;rsquo;t need a forty-page architecture document for a URL shortener, you only need the handful of decisions that would be genuinely expensive to get wrong:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-agent-architect
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-architecture
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The decisions worth pinning down here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; how do we persist &lt;code&gt;code → target_url + expires_at&lt;/code&gt;? Key-value store, relational table, in-memory for a toy?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expiry enforcement:&lt;/strong&gt; lazy (check &lt;code&gt;expires_at&lt;/code&gt; on read) or active (a sweeper that deletes expired rows)?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ID generation:&lt;/strong&gt; how we mint short codes and handle collisions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lazy-versus-active call is the one with real teeth in it, and here is how &lt;code&gt;architecture.md&lt;/code&gt; records that call, capturing both the decision &lt;em&gt;and&lt;/em&gt; its consequence:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Expiry enforcement: lazy (checked on read)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Chosen: lazy.** Simplest correct behavior; no scheduler, no clock daemon.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Consequence, on purpose:** an expired link&amp;#39;s record still &lt;span class="ge"&gt;_exists_&lt;/span&gt; in the
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; store until something evicts it. &lt;span class="sb"&gt;`resolve`&lt;/span&gt; must therefore never assume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;present in store&amp;#34; means &amp;#34;live&amp;#34;. It must evaluate expiry every time.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Rejected for v1: active sweeping. Adds a scheduler we don&amp;#39;t need yet.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Writing down the &lt;em&gt;consequence&lt;/em&gt;, namely that &amp;ldquo;present in store&amp;rdquo; does not mean &amp;ldquo;live&amp;rdquo;, is the part that pays off later on, because it&amp;rsquo;s the exact invariant that the implementation is going to have to honor, and it has been stated in plain language before a single line of &lt;code&gt;resolve&lt;/code&gt; even exists. The output of all this is an &lt;code&gt;architecture.md&lt;/code&gt; sitting in your repository, reviewable and versioned, the kind of thing a teammate can simply read instead of having to reverse-engineer it back out of the code.&lt;/p&gt;
&lt;h2 id="phase-4-the-story-packaging-context-for-the-implementer"&gt;Phase 4, The story: packaging context for the implementer
&lt;/h2&gt;&lt;p&gt;This is the phase that vibe coders have quite simply never seen, and it also happens to be the one that makes the whole method actually work.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-create-epics-and-stories # break the PRD into epics + stories
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-create-story # generate the next story with full context
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A &lt;strong&gt;story&lt;/strong&gt; is a single, self-contained unit of work, but the genuinely important part is &lt;em&gt;what&amp;rsquo;s actually in it&lt;/em&gt;, because a BMAD story file is nothing like a Jira ticket that carries a title and a single sentence of description. It is instead a complete context package, gathering together the relevant requirements, the architectural decisions that happen to apply, the acceptance criteria and pointers to the exact files that are in play, and it is written in such a way that an agent, or for that matter a human, can pick it up completely cold and still have everything they need to get started.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example &lt;code&gt;story-1.3&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# Story 1.3 — Return 410 for expired links
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Context
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Implements FR-3 (see prd.md). Expiry is enforced lazily on read, and the
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;boundary instant is inclusive (now &amp;gt;= expires_at ⇒ expired) — both are
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;architecture decisions. &lt;span class="sb"&gt;`now`&lt;/span&gt; is injected into resolve() so expiry stays
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;deterministic in tests.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Acceptance criteria
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; resolve of a link whose expires_at &amp;lt;= now → 410, body {&amp;#34;reason&amp;#34;: &amp;#34;expired&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; no 3xx is produced for an expired link
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; a link with expires_at in the future still redirects 302 (regression guard)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; a link with expires_at is None still redirects 302 (never expires)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; at exactly expires_at, the link is already expired (boundary is inclusive)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;- [ ]&lt;/span&gt; unknown code still returns 404 (unchanged)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Files in play
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; shortener/service.py (add &lt;span class="sb"&gt;`now`&lt;/span&gt; param to resolve; add expiry branch)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; shortener/http_app.py (supply now = datetime.now(timezone.utc))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; tests/test_service.py (expiry + regression cases)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Notice how the criteria carry all of the upstream decisions through verbatim, including the inclusive boundary, the &amp;ldquo;no 3xx&amp;rdquo; rule and the regression guards, so that nothing at all has been left open to interpretation.&lt;/p&gt;
&lt;p&gt;Why does any of this matter so much? It matters because when you hand &lt;em&gt;this&lt;/em&gt; to an implementer there is genuinely nothing left for them to guess at, since the &lt;code&gt;410&lt;/code&gt; is no longer a coin flip but an acceptance criterion with a regression guard sitting right next to it, and the story turns out to be the vehicle that carries every last decision you made back in phases 1 through 3 forward into the code itself.&lt;/p&gt;
&lt;p&gt;With the story now in hand, all of the planning artifacts are finally in place, and before writing any code at all it is worth taking a moment to check that they actually line up with one another:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-check-implementation-readiness
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This validates that your PRD, your architecture and your stories all agree with one another before anyone starts implementing anything, which is really the spec-driven equivalent of the old carpenter&amp;rsquo;s rule to measure twice and cut once.&lt;/p&gt;
&lt;h2 id="phase-5-implementation-now-finally-code"&gt;Phase 5, Implementation: now, finally, code
&lt;/h2&gt;&lt;p&gt;Only now do we finally sit down to write code, and by this point the act of &amp;ldquo;writing the code&amp;rdquo; has become almost mechanical, precisely because every meaningful decision was already made further upstream.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-agent-dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or run a story end to end:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-dev-story
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Amelia, the developer, works &lt;em&gt;from the story&lt;/em&gt;, implementing against the acceptance criteria one by one, and the real tell that you are no longer vibe coding is that she can check her own work against a written definition of done rather than against her own intuition. Whenever she finds herself uncertain about something, the answer is already sitting in the story, or in the PRD, or in the architecture document, and never in a guess.&lt;/p&gt;
&lt;p&gt;The entire implementation of story 1.3 is this diff to &lt;code&gt;resolve&lt;/code&gt;, and because the story pre-decided everything the change is almost mechanical:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;-def resolve(store: InMemoryStore, code: str) -&amp;gt; Resolution:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;#34;&amp;#34;&amp;#34;Resolve a short code to a redirect or an error (FR-2, FR-4).&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gi"&gt;+def resolve(store: InMemoryStore, code: str, *, now: datetime) -&amp;gt; Resolution:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;#34;&amp;#34;&amp;#34;Resolve a short code to a redirect or an error (FR-2, FR-3, FR-4).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ Expiry is evaluated lazily on every read: a record present in the store is
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ not necessarily live. The boundary is inclusive — at exactly ``expires_at``
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ the link is already gone.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;&lt;/span&gt; link = store.get(code)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if link is None:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return Resolution(status=404, body={&amp;#34;reason&amp;#34;: &amp;#34;not_found&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ if link.expires_at is not None and link.expires_at &amp;lt;= now:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ return Resolution(status=410, body={&amp;#34;reason&amp;#34;: &amp;#34;expired&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;&lt;/span&gt; return Resolution(status=302, location=link.target_url)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;It comes down to two lines of logic, but the part worth looking at is the docstring, because it cites FR-3, the lazy-expiry decision and the inclusive boundary all together, and it can do so only because all three of them came &lt;em&gt;down the pipe&lt;/em&gt; from the artifacts that preceded it. The code, in other words, is not the place where any of those decisions were made, it is simply the place where they were finally applied.&lt;/p&gt;
&lt;p&gt;Contrast this with the ordinary vibe-coding loop, where you prompt the model, eyeball whatever output comes back and then re-prompt it whenever the result is wrong, because here the loop instead becomes implement against the criteria, verify against the criteria and you&amp;rsquo;re done. The AI is of course still doing all of the actual typing, but this time it is typing toward a target that you defined in advance rather than toward some vague sense of &amp;ldquo;looks about right.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="phase-6-review-verify-against-the-spec-not-against-vibes"&gt;Phase 6, Review: verify against the spec, not against vibes
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-code-review
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The review is what closes the loop, and here is the quiet superpower that comes from having done all of the upstream work first, which is that &lt;strong&gt;you can now review against the spec itself&lt;/strong&gt;. A question like &amp;ldquo;does the expired-link path return &lt;code&gt;410&lt;/code&gt; in the way that FR-3 requires?&amp;rdquo; has become a plain yes-or-no question, whereas without a spec to lean on, code review tends to degenerate into &amp;ldquo;looks fine to me&amp;rdquo;, which is really just vibe coding wearing a blazer.&lt;/p&gt;
&lt;p&gt;And this is exactly where the whole approach earned its keep, because the review caught a real defect, and that defect was only catchable in the first place &lt;em&gt;precisely because there was a spec to check the code against&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; &lt;code&gt;architecture.md&lt;/code&gt; states an invariant — &amp;ldquo;timestamps are UTC-aware; naive datetimes are a caller error&amp;rdquo; — but nothing enforces it. &lt;code&gt;create_link&lt;/code&gt; happily accepts a naive &lt;code&gt;expires_at&lt;/code&gt;. At resolve time, &lt;code&gt;expires_at &amp;lt;= now&lt;/code&gt; then compares a naive datetime against the adapter&amp;rsquo;s aware &lt;code&gt;now&lt;/code&gt; and raises &lt;code&gt;TypeError: can't compare offset-naive and offset-aware datetimes&lt;/code&gt;. FR-3 promised a &lt;code&gt;410&lt;/code&gt;; the caller gets an unhandled &lt;code&gt;500&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single bug is the whole argument for spec-driven development compressed into one example, because a vibe-coded shortener would almost certainly have carried &lt;em&gt;the very same defect&lt;/em&gt;, and yet with no &lt;code&gt;architecture.md&lt;/code&gt; invariant for it to violate, the reviewer would have had nothing to notice it against in the first place. The spec did more than merely document the decision, it turned what would have been a silent 500 into a visible finding, and the fix itself came down to a three-line guard in &lt;code&gt;create_link&lt;/code&gt; that fails fast with a clear error, together with a regression test, and because the invariant had been written down all along, the question &amp;ldquo;is this actually fixed?&amp;rdquo; is once again a simple yes-or-no.&lt;/p&gt;
&lt;h2 id="what-actually-just-happened"&gt;What actually just happened
&lt;/h2&gt;&lt;p&gt;Step back and look at what we&amp;rsquo;ve produced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brief.md&lt;/code&gt;: the ideas and the decisions we deferred&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prd.md&lt;/code&gt;: the requirements, testable&lt;/li&gt;
&lt;li&gt;&lt;code&gt;architecture.md&lt;/code&gt;: the technical calls&lt;/li&gt;
&lt;li&gt;&lt;code&gt;story-1.3.md&lt;/code&gt;: the context package that drove the code&lt;/li&gt;
&lt;li&gt;the code itself, plus tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code is the &lt;em&gt;smallest&lt;/em&gt; artifact in that entire list, and it is also the very last one we made, and that inversion is really the whole point of the exercise. In vibe coding the code is the first and only artifact you ever produce, so every decision ends up trapped inside it and left undocumented, whereas in spec-driven development the code is instead a &lt;em&gt;consequence&lt;/em&gt; of everything else, with the decisions themselves living in readable documents and the code standing as nothing more than their current implementation.&lt;/p&gt;
&lt;p&gt;This is exactly why the friction turns out to be worth it, and in a real sense the friction &lt;em&gt;is&lt;/em&gt; the value, because every question that Mary asked, every requirement that John pinned down and every call that Winston made are all decisions that would otherwise have been made silently by an autocomplete, at twice the speed and none of the visibility.&lt;/p&gt;
&lt;p&gt;You didn&amp;rsquo;t actually slow down at all, you simply moved the thinking to the front of the process where it happens to be cheap, instead of leaving it for the debugger later on where it is always expensive.&lt;/p&gt;
&lt;h2 id="try-it-on-something-small"&gt;Try it on something small
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t go and start with your production monolith. Pick a feature that is exactly this size, small enough that you can finish it in a single sitting and yet real enough that it still hides a few decisions inside it, and then run the full loop through once from beginning to end:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-brainstorming # surface the decisions
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-prd # write them down
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-architecture # make the technical calls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-create-story # package the context
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-dev-story # implement against it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bmad-code-review # verify against the spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The first time you go through it, the whole process will feel like pure overhead, and you should do it anyway, because somewhere around the story phase, at the moment when you hand the implementer a spec and get back exactly what you asked for, it quietly stops feeling like overhead and starts feeling like the thing you had been missing all along.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the moment the lever stops being a slot machine.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Want the backstory on why I stopped vibe coding? Start with &lt;a class="link" href="https://bstz.it/p/dont-be-mad-bmad-instead/" &gt;Don&amp;rsquo;t be mad, BMAD instead&lt;/a&gt;. Want to meet the agents properly? &lt;a class="link" href="https://bstz.it/p/bmad-meet-the-crew/" &gt;Here&amp;rsquo;s the crew&lt;/a&gt;. Want to see the method on a bigger project? &lt;a class="link" href="https://bstz.it/p/bmad-in-action-building-tododoro/" &gt;Building TODOdoro&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>