The ticket as data
Run the code above. That's a real build ticket, minus the parts specific to your product — and the loop at the bottom is the beginning of a linter that will hold every ticket you write.
Walk the fields:
goal— one sentence, outcome-shaped. Not "add validation" (an activity) but "form validates tax IDs before submit" (a state of the world you can observe). If the goal names an activity, the builder can complete the activity without producing the outcome, and technically it did what you asked.out_of_scope— the scope fence. With human teams the fence is social: everyone knows the redesign is someone else's epic. An AI builder knows nothing socially. Given half an excuse it will "improve" the approvals workflow while it's in there, and now your diff touches a system nobody reviewed it for. Whatever isn't fenced is in play.owner— a name, singular. Not "the team." When the acceptance review happens — and lesson two is entirely about that review — someone specific either ran it or didn't. Deliverables without owners are how deliverables ship unreviewed.acceptance— the load-bearing list, written before the build. Each check must be testable: a person (or a script, or the builder itself) can look at the output and say pass or fail without asking you what you meant. A1 is testable — feed it an 8-digit ID, look for the error. A2 is testable — run the fixtures, count. A3 just got flagged by the lint, and rightly: "feels clean and modern" is not a check, it's a future argument scheduled for review day.
Why phrases, not concepts
Notice the VAGUE list is words, not ideas. That's deliberate, and it's the same trick a spam filter uses: a script can substring-match "robust"; it cannot substring-match "this criterion has no observable pass condition." The word list is a cheap proxy that catches most offenders — "clean," "modern," "intuitive," "feels" — and every hit forces the same fix: rewrite the check until it names something observable. Cheap proxies are blunt: substring matching means "cleanup" trips "clean," and the moment "fast" joins the list, "breakfast" trips too. Fine. A false positive costs one shrug and an override; the false negative it prevents costs a build. When the lint flags something silly, you overrule it — the lint works for you, not the other way around. "Feels clean" becomes "form renders without horizontal scroll at 375px width." Suddenly the builder can verify its own work, which means it converges instead of guessing.
A ticket that survives this lint has a specific property worth naming: it can be executed and verified by someone who was never in the meeting. That's the property AI builders require and vague tickets lack. The next six steps drill it until writing tickets this way is faster than writing them the old way — because it is, once you count the rework you're not doing.