Friday hit 4:41 p.m. on my laptop clock.
My tiny CLI finally returned a clean summary.
A coworker wanted it on Monday morning.
He would paste real tickets with real names.
He would paste messy Slack threads without thinking.
I froze with a green test suite in front of me.
The merge checks were green too, somehow.
So what did production-ready even mean tonight?
Green tests still do not mean production-ready, right?
They mean the happy path survived one laptop.
I needed a checklist that could fail closed.
I wanted a file a teammate could copy.
This post is that file.
Copy it, break it, or trash it when a gate stays red.
The constraint I actually had
I ship small AI CLIs as a solo builder.
There is no platform team behind me.
I sometimes point them at free model access.
I sometimes park runners on a free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode currently offers both of those pieces.
I treat them as a budget, not a promise.
I will not name models here.
I will not quote quotas that can move overnight.
The checklist does not care who hosts the weights.
It cares what you can prove before a real user.
Time box: ninety minutes on a weekday evening.
Extra spend: zero dollars beyond my existing machine.
If a gate needs a paid SLA, I stop.
Abandonment is cheaper than a silent wrong summary.
What production means for this CLI
This is not five nines of uptime.
This is not a public status page either.
It means one person can run the same flags twice.
The shape stays stable. The exit codes stay boring.
A bad model answer must not leak a secret.
It must not hang the shell until lunch.
It must not print success around empty JSON.
Have you seen a CLI succeed on empty JSON?
The artifact: evidence JSON plus a checker
I keep evidence in one JSON file.
The checker is boring Python on purpose.
If a required field is missing, we do not ship.
Fail closed. Skip the maybe-Monday comments.
{
"cli_name": "ticket-summarize",
"contract_version": "2026-09-05.1",
"timeout_ms": 8000,
"max_input_chars": 4000,
"redaction_patterns": ["sk-", "Bearer ", "-----BEGIN"],
"empty_output_policy": "fail",
"rollback_cmd": "git revert --no-edit HEAD && npm unlink ticket-summarize",
"abandon_if": [
"timeout_ms > 15000",
"cannot pin contract_version",
"no local fixture for empty output"
]
}
Save it as ship/readiness.json beside the CLI.
Wikis rot. A file next to the binary does not.
Gate 1: Freeze the contract before the model
What does success look like in one object?
I only allow title and next_action fields.
I write the contract first on purpose.
Then I let the model fill those two fields.
# contract.py
from pydantic import BaseModel, Field
class Summary(BaseModel):
title: "str = Field(min_length=8, max_length=80)"
next_action: str = Field(min_length=8, max_length=160)
If the model adds helpful extra keys, I drop them.
Why keep surprises in a ticket summary?
Fail closed: invalid schema exits with code 2.
Exit 0 is reserved for a valid Summary object.
Gate 2: Pin a timeout you can actually feel
Eight seconds is my default ceiling.
Users wait a bit, then they hit Ctrl-C.
A free server can stall without warning.
Your shell should not stall with it.
import urllib.request
import sys
req = urllib.request.Request(url, data=payload, method="POST")
try:
with urllib.request.urlopen(req, timeout=8) as resp:
body = resp.read(64_000)
except TimeoutError:
sys.exit("timeout: abandon this run")
Need more than fifteen seconds for one summary?
I abandon the production idea the same night.
Why ship a hang dressed up as intelligence?
Gate 3: Empty output is a checked failure fixture
I keep fixtures/empty.json in the repo.
The fixture file contents are literally empty braces.
Every build runs the parser against that fixture.
Does it exit 2? If not, the gate is red.
python parse_summary.py fixtures/empty.json
echo $? # must print 2
I use the same exit code for truncated JSON.
I refuse any best-effort sentence on failure.
Here is the parser I actually keep.
# parse_summary.py
import json, sys
from contract import Summary
raw = open(sys.argv[1]).read().strip()
if not raw:
sys.exit(2)
try:
data = json.loads(raw)
Summary.model_validate(data)
except Exception:
sys.exit(2)
print("ok")
If this gate is red, the CLI stays on my laptop.
No coworker. No Monday. No fake confidence.
Gate 4: Pin the prompt, not a vibe
I version the prompt like application code.
That means a file plus a hash.
sha256sum prompts/summarize.v1.txt > ship/prompt.sha256
Store that hash inside readiness.json too.
Prompt drift is a ship blocker, not a footnote.
Do I know which model slot answered me?
I log the slot id the runtime gave me.
I do not hard-code vendor names I cannot verify.
The log line is enough evidence for a solo shop.
import hashlib, os, sys
def hash_file(path):
data = open(path, "rb").read()
return hashlib.sha256(data).hexdigest()
evidence["prompt_sha256"] = hash_file("prompts/summarize.v1.txt")
evidence["slot_id"] = os.environ.get("MODEL_SLOT", "unset")
if evidence["slot_id"] == "unset":
sys.exit("fail-closed: no slot id")
No slot id means no production run.
Guessing is not a release strategy, is it?
Gate 5: Redact before the packet leaves the laptop
Free servers are other people's computers.
Treat every paste as hostile input.
I strip tokens, bearer headers, and PEM banners.
Then I send the leftover text, capped at 4000 chars.
import re
SECRETS = [
r"sk-[A-Za-z0-9]+",
r"Bearer\s+\S+",
r"-----BEGIN [A-Z ]+-----",
]
def redact(text: str) -> str:
out = text
for pat in SECRETS:
out = re.sub(pat, "[REDACTED]", out)
return out[:4000]
If redaction wrecks the ticket meaning, I abort.
I do not send a hollow string and call it done.
Would you paste a customer key into a free model?
I would not. The gate exists so I cannot forget.
Gate 6: Record wall clock and size, then cap both
I do not invent token prices in this checklist.
I record wall clock and character counts only.
evidence["wall_ms"] = elapsed
evidence["chars_in"] = len(payload)
evidence["chars_out"] = len(body)
if evidence["wall_ms"] > 8000:
sys.exit("fail-closed: over time budget")
Ninety minutes to wire the checklist itself.
Zero extra dollars on top of the free path.
If the free server disappears tomorrow, I still have fixtures.
I demo from fixtures, or I stop talking about Monday.
Abandonment is a feature here.
It is not a personality flaw.
Gate 7: One rollback command you already typed
A checklist without rollback is a mood board.
I refuse to ship on vibes.
I keep a command I have run on a throwaway clone.
The dry-run log lives in ship/.
git revert --no-edit HEAD
npm unlink ticket-summarize
hash -r
If unlink makes you nervous, skip global links.
Use a directory prefix instead.
alias ticket-summarize="$PWD/bin/ticket-summarize"
# rollback:
unalias ticket-summarize
Can you undo this in two minutes flat?
If not, it is not production-ready yet.
Gate 8: An evidence pack a teammate can re-run
I dump a folder, not a screenshot.
I do not accept a Slack screenshot as evidence.
I do not accept my own memory as evidence.
ship/
readiness.json
prompt.sha256
fixture.empty.exit.log
timeout.log
redact.sample.txt
rollback.dry-run.log
The checker only reads this folder.
It never trusts my recollection of last night.
# prod_ready.py
import json, pathlib, sys
root = pathlib.Path("ship")
need = [
"readiness.json",
"prompt.sha256",
"fixture.empty.exit.log",
"timeout.log",
"redact.sample.txt",
"rollback.dry-run.log",
]
missing = [n for n in need if not (root / n).exists()]
if missing:
print("missing:", ", ".join(missing))
sys.exit(2)
data = json.loads((root / "readiness.json").read_text())
if data.get("empty_output_policy") != "fail":
sys.exit("fail-closed: empty output must fail")
if int(data.get("timeout_ms", 99999)) > 15000:
sys.exit("fail-closed: timeout too loose")
print("production checklist: green")
Run it from the repo root.
python prod_ready.py
Red output means I message the coworker immediately.
The message is short: not this week.
The ninety-minute sequence
- Write
contract.pyand the empty fixture. - Add
parse_summary.pyand confirm exit 2. - Wrap the HTTP call in an eight-second timeout.
- Hash the prompt and store it under
ship/. - Run redaction on a sample ticket with a fake key.
- Type the rollback command once and save the log.
- Run
prod_ready.pyand stop on any missing file.
That list is the whole evening ritual.
There is no architecture review on this path.
There is no extra committee on the calendar.
Where a free model and a free server fit
I use them on the canary path only.
I keep customer secrets off that path.
I keep payroll notes off that path too.
MonkeyCode's free model access and free server option can exercise gates 2 through 6.
Those options are not an uptime contract.
They are a networked sandbox I can fail against.
If a gate needs guaranteed uptime, I wait.
I ship a fixture-only demo instead.
Want to point the same checker at a free slot?
Export MODEL_SLOT, drop files in ship/, and run prod_ready.py.
That is the only invite in this post.
Who should not use this
Do not use this checklist for medical text.
Do not use it for legal advice or payroll files.
Do not use it if you cannot redact pastes.
Do not use it if you need a streaming UI.
Do not use it to hide a missing contract.
The contract is the product, not the model.
Teams with a real SRE rotation have better gates.
They can steal the empty fixture idea.
They can ignore the rest of this post.
Limitations I will not hide
The checker cannot see silent prompt drift on a host.
It only sees the hash you stored locally.
A free server can change answers between runs.
The timeout gate catches hangs, not clever nonsense.
Schema checks catch shape, not a wrong next action.
A valid object can still be unhelpful.
This is production-readiness for a solo CLI.
This checklist is not a compliance program.
This checklist is not a vendor audit either.
Fail-closed criteria, one copyable list
- Missing
ship/file: exit 2. - Empty or truncated model output: exit 2.
- Run over 8s, or config over 15s: abandon.
- Unset slot id: abandon.
- Redaction would destroy the ticket: abort the send.
- Rollback never dry-run: do not link the binary.
If any bullet turns into an argument, the answer is no.
I used to argue with myself at 4:41 p.m.
The file argues faster than I do.
What field is missing from your readiness.json?
Tell me the constraint. I will add the gate before the next ship.
Top comments (0)