11. Release gates — the line where 'done' becomes a fact
What a release gate is, why 'done' rots without one, and the day a track passed every check yet shipped 42 items short of the goal.
Part 10 — PRD ended on a single line. A PRD describes the destination.
This one is about how the operator knows they arrived.
Release gates. The concrete, atomic, machine-checkable conditions that turn "done" from a feeling into a fact. Without them, the PRD is a wish. With them, it is a road with mile markers, each one verifiable by something other than the operator's confidence.
I believe the most important property of a release gate is that it is unambiguous to a machine. Not because human judgment doesn't matter — the Manual-Gate rows are for exactly that — but rather because what a gate actually does is remove "done" from the category of things that can be lied about, even unintentionally.
0. The premise — "done" is the most expensive word
Part 7 — Verification loops made the case at the code level. The moment the operator feels "that's done," lint and build and tests fire automatically before the report can land. That move shifts a fragile word — "done" — from operator memory to system enforcement.
A release gate is the same move, one level up.
At code level, "done" means this slice compiles and its tests pass. At release level, "done" means the system is allowed to be live. The two are not the same word and they cannot be enforced by the same mechanism.
A passing test does not tell you the privacy policy exists. A passing build does not tell you the OAuth redirect URIs are whitelisted. A clean lint does not tell you the operator's real name was scrubbed from every layer of metadata. Those questions belong to a different gate.
1. What a release gate actually is
A release gate is a table.
Each row of the table is one atomic, externally verifiable condition. Each row has a status:
✅ pass— verified, this row currently holds⏳ in progress— being worked on, not yet held🔒 waiting on operator— blocked on a human-only step (a Dashboard click, an account sign-up, a DNS change)❌ not started— known requirement, no movement—not applicable — the row was added speculatively and the situation made it irrelevant
The table is part of the PRD. It does not live in a separate document. The reason is the same one from Part 10 §4 — the shape of the road is built into the destination doc. No one needs to ask "what counts as done?" because the document answers.
The gates are cumulative. Gate 1 assumes Gate 0 passed. Gate 9 assumes 0 through 8. The order is not preference, it is dependency — you cannot pass Gate 2 (infrastructure) before Gate 1 (compliance) any more than you can launch a service before you have a domain.
A release gate is not a checklist. It is the contract between intent and reality.
2. Auto-Gate vs Manual-Gate
Not all gate rows are the same kind of thing. Two kinds, and the difference matters.
2.1 Auto-Gate rows — what a machine can verify
Auto-Gate rows are conditions a machine can verify. Examples:
pnpm buildexits 0pnpm lintexits 0pnpm testexits 0grep -r "operator-real-name" .returns nothingcurl -I https://devalice.jaceclub.comreturns 200verify:assetsscript reports 15/15 SHA-256 matches
If the row can be expressed as a shell command that exits 0 or non-zero, it belongs in the Auto-Gate set. The system can fire it on every commit, every push, every nightly cron — without the operator being awake.
2.2 Manual-Gate rows — what only a human can decide
Manual-Gate rows are conditions only a human can decide. Examples:
- "The privacy policy reads correctly in both languages"
- "Lead approves the M2 milestone definition"
- "The seed content set covers five distinct user pains"
- "OAuth scopes look appropriate for the trust level"
- "Lighthouse 'Best Practices' findings are acceptable trade-offs"
These rows wait for a person. They are the part of the gate that cannot be automated, and pretending otherwise is the source of most release accidents.
The discipline is:
| Discipline | Reason |
|---|---|
| Every Auto-Gate row has a script — and CI runs it | Otherwise the row exists on paper but not in practice |
| Every Manual-Gate row names who approves — not just "approved" | Otherwise it gets approved by no one and counted as passing |
| Auto and Manual are visually distinct in the table | The operator should not have to remember which is which |
| Manual approvals leave a trace (commit, comment, PR review) | Memory rots; commits do not |
3. The cumulative pattern in practice
A real-shaped gate table from a real project — devalice — in its abbreviated form:
Gate 0: M0 launch (temporary domain)
- pnpm build passes [Auto] ✅
- Vercel preview deploy [Auto] ✅
- Category routes × 4 [Auto] ✅
- Seed guide × 4 [Manual] ✅ (lead reviewed 2026-05-10)
- ESLint clean [Auto] ✅
- Vercel env vars set [Manual] 🔒 (operator)
Gate 1: Compliance
- Privacy Policy (ko + en) [Manual] ✅
- Content license (CC BY) [Auto] ✅ (file exists, header text matches)
- Code license (MIT) [Auto] ✅
- About page exists [Auto] ✅
- Cookie notice (PIPA) [Manual] ✅
Gate 2: Infrastructure
- Custom domain bound [Manual] 🔒
- SSL active [Auto] — (depends on Gate 2 row 1)
- Production env split [Manual] 🔒
Gate 3: Security
- No hardcoded secrets [Auto] ✅ (grep)
- .env.example complete [Auto] ✅
- RLS enabled [Manual] 🔒 (waits on DB push)
- OAuth redirect whitelist [Manual] 🔒
- Input validation [Auto] ✅
- Asset SHA-256 match [Auto] ✅ (verify:assets 15/15)
...
Three things this table does that prose cannot.
Status is visible at a glance. Scan the column. Count the ✅ versus the 🔒. The picture is immediate. No one has to read three paragraphs to know "where are we."
Blockers self-identify. A 🔒 row in Gate 2 is the next thing that has to happen. The table tells the operator and the lead and the next session what to look at first.
Gates self-order. "Do we ship?" reduces to "is Gate N's last 🔒 cleared?" The argument about timing is over before it starts.
4. The day "done" was a lie — the cost of gates not enforced in code
This is the story I keep coming back to when someone asks why we go to the trouble.
In another project — call it Track D — four sub-phases shipped in sequence over several weeks. D-0, D-1, D-2, D-3. Each one reported "success" at the end. lint passed. build passed. cargo test passed. The Lighthouse score was fine. The team lead read the reports and moved on.
Several weeks later, during a routine audit, we found that the actual deliverables were 42 items short across the four sub-phases. Not bugs — missing structural commitments. Four UI primitives that the spec said would exist were not yet wired. Eight queue states that the spec said would be decomposed had not been. Seven files that the spec said would be deleted were still in the tree. Nine legacy backend handlers that the spec said would be removed were still being hit.
How did this happen? Every report was honest. The deliverable did compile. Tests did pass. The reporter — a delegated agent in this case — had no signal that anything was wrong.
The reason was simple. The promises in the spec were prose. They were not enforced in code. Nothing fired a check that said "the spec named four UI primitives — do four UI primitives exist in the tree?" Nothing fired a check that said "the spec said file X is deleted — is file X gone?" The verification harness checked compilation, not commitments.
The fix was a sub-phase nobody wanted to do: D-Catchup. Go back. Verify each promise against the tree. Wire what was missing. Delete what should have been gone. Then — and this is the durable change — add a verify:d-N script to every future delegation, and put it on the merge gate, so the next four sub-phases could not lie to us even if they wanted to.
This is the lesson:
A promise is not a release gate. A promise enforced in code is.
After D-Catchup, every delegation in that project gets three things in the same PR:
- The delegation brief — what, why, how
- A
verify:phase-N.mjsscript that walks every promise and exits non-zero on a miss - A CI rule that blocks merge unless
pnpm run verify:phase-Nexits 0
That is a release gate at delegation scale. Same shape as the M0–M10 gates in the PRD, just applied to a smaller unit.
5. The shape of a verify script
A verify script is small and dumb on purpose. It is not a test framework. It is a literal walker.
For each row of the spec, one block:
// Row D-1.3 — Queue must be decomposed into 8 states
const queueStates = readQueueStates();
if (queueStates.length !== 8) {
fail("Queue has " + queueStates.length + " states, expected 8");
}Three properties matter.
5.1 1:1 with the spec
Row 3 of the spec gets check 3 of the script. If a row is added to the spec, the script does not pass until a check is added. The two documents move in lockstep.
5.2 Output is the receipt
The reporter does not say "verified." They paste the raw stdout. verify:d-1 passed 18 / 18. If the line is not in the report, the report is not accepted.
5.3 Failure is loud and specific
"Queue has 5 states, expected 8" beats "verify failed." The operator should know what is missing without opening the script.
The script is hand-written. Not generated. Not framework-driven. It is the operator's reading of the spec, expressed in code, sitting on the merge gate.
6. Manual-Gate rows — how to keep them honest
Auto-Gate rows are easy to keep honest. CI runs the check and either passes or fails. Manual-Gate rows are where the discipline shows.
The patterns I use:
Every Manual row names the approver. Not "approved by team" — name the role and date. Manual ✅ lead, 2026-05-10. If the approver changes, the row goes back to ⏳ until the new approver re-approves.
Manual rows leave a commit trail. The commit that flips a row from ⏳ to ✅ includes the approver's name in the message and a one-line note on what they reviewed. [devAlice] chore(gate-1): privacy policy ko/en — lead approved. Six months later, when someone asks "wait, who decided this was okay?" the git log answers.
Manual rows have an explicit re-review cadence. A privacy policy approved in M0 is not approved forever. The PRD names a re-review trigger — re-review on M2 launch or re-review every two quarters. When the trigger fires, the row flips back to ⏳ until re-confirmed.
No Manual row uses "I'll get to it" as a status. Either there is an owner and a deadline or it is ❌.
7. The traps
Release gates fail in predictable ways.
7.1 Gates that grow but never close
A Gate 3 table accretes rows for two quarters and never has a moment where every row is ✅. That is not a gate, that is a wish list. Split it. The non-blocking rows become "Gate 3 part B" or get demoted to "post-launch tracking." A gate's purpose is to close.
7.2 Gates that pass without a check
A row reads Auto ✅ but there is no script anywhere that exercises it. Two weeks later it does not pass at all and nobody knew. Every Auto row needs a runnable check, named in the row. If the check does not exist, the row is ❌, not ✅.
7.3 Promises that never become gates
This is the D-Catchup pattern. The spec says X will be true. Nothing checks that X is true. The reporter is honest about what they verified, which is what they knew about — the build, the tests, the lint. The thing nobody verified is the thing that breaks. Anything in a spec worth promising is worth gating.
7.4 Manual approvals that "happened verbally"
A row reads Manual ✅ because a lead said "looks good" in a session three weeks ago. Nothing in git records it. The next time someone asks why it is passing, the trail is gone. Every Manual approval leaves a commit or a PR review. If it is not in the repo, it did not happen.
7.5 The "we'll test it on production" gate
A row reads pending — will verify after launch. That is not a release gate, that is a post-mortem in waiting. Either it is verifiable before launch (move it to ⏳ and verify it) or it is not a launch criterion (move it to a separate "post-launch tracking" section).
8. One principle
The whole shape of release gates collapses to one line.
"Anything the operator's mind would treat as 'done,' make the system check first. Anything the system cannot check, name the human who did."
Release gates are the layer where the PRD stops being a wish and becomes a contract. Auto-Gate rows are the contract enforced by a machine. Manual-Gate rows are the contract enforced by a named human leaving a trace. The combination is what the operator means when they say a slice is live.
And once the gates are real — once every row is either a script that exits 0 or a human approval recorded in git — most of the daily work stops needing the operator at the keyboard. That is what Part 12 — Ralph loop is about. A PRD says what to build. A gate says when it is built. A loop walks the road from one to the other, on its own, 24/7, while the operator sleeps.