DEV Community

Sam Rivera
Sam Rivera

Posted on

Your Readiness Checklist Expires: Add Freshness Gates to a Tiny AI CLI

Last Thursday at 11:40pm I promoted a build. My checklist said "tests pass."

The evidence file was 26 days old. Nobody noticed. The list was green and the deploy was wrong.

There is a loud argument on DEV right now about whether AI writes better code than developers. It misses the boring part. The generated code was fine — my evidence was rotten.

So today I want to kill one specific failure mode: checklists that never expire.

Why a static checklist lies

A checklist is a promise about the past. It means "at some point, someone verified this."

Then time passes. Configs move. Artifacts get overwritten. The green checkmark survives because nothing checks the checkmark.

So your gate is not evidence. It is a memory of evidence. Would you ship on a 26-day-old test log? I did, and I only found out from a support ping.

I wanted three properties, and nothing more:

  1. Freshness — every artifact has a maximum age in days.
  2. Integrity — the file is the same file a human reviewed.
  3. Fail-closed — a missing file blocks; it never warns and continues.

Step 1 — Write the gate file

Four gates. That is the whole list for a small CLI.

{
  "gates": [
    {"id": "unit-tests",     "evidence": "artifacts/pytest.txt",   "max_age_days": 3,  "owner": "sam"},
    {"id": "smoke-cli",      "evidence": "artifacts/smoke.txt",    "max_age_days": 7,  "owner": "sam"},
    {"id": "cost-run",       "evidence": "artifacts/cost.txt",     "max_age_days": 14, "owner": "sam"},
    {"id": "rollback-drill", "evidence": "artifacts/rollback.txt", "max_age_days": 30, "owner": "sam"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

owner is the person who refreshes the evidence. An empty owner is an invalid gate. That rule alone removed two zombie items from my list.

Notice what is missing: no severity field, no scoring, no dashboard. Those turn a gate into a discussion.

Step 2 — Drop in a runner that fails closed

This is about 70 lines, standard library only.

#!/usr/bin/env python3
"""gatecheck.py - readiness gates with expiry and integrity."""
import argparse, hashlib, json, os, sys, time

LOCK = "gates.lock.json"


def sha256(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def seal(cfg_path: str, lock_path: str) -> None:
    cfg = json.load(open(cfg_path, encoding="utf-8"))
    lock = {"sealed_at": int(time.time()), "hashes": {}}
    for gate in cfg["gates"]:
        p = gate["evidence"]
        lock["hashes"][gate["id"]] = sha256(p) if os.path.exists(p) else None
    with open(lock_path, "w", encoding="utf-8") as f:
        json.dump(lock, f, indent=2)
    print(f"sealed {len(lock['hashes'])} gates -> {lock_path}")


def check(cfg_path: str, lock_path: str, now: float | None = None) -> list[tuple[str, str]]:
    cfg = json.load(open(cfg_path, encoding="utf-8"))
    lock = json.load(open(lock_path, encoding="utf-8"))
    now = now or time.time()
    failures: list[tuple[str, str]] = []
    for gate in cfg["gates"]:
        gid, path, max_age = gate["id"], gate["evidence"], gate["max_age_days"]
        if not gate.get("owner"):
            failures.append((gid, "NO OWNER"))
            continue
        if not os.path.exists(path):
            failures.append((gid, "MISSING"))
            continue
        age_days = (now - os.path.getmtime(path)) / 86400
        if age_days > max_age:
            failures.append((gid, f"STALE {age_days:.1f}d > {max_age}d"))
            continue
        if lock["hashes"].get(gid) != sha256(path):
            failures.append((gid, "CHANGED since seal"))
    return failures


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("mode", choices=["seal", "check"])
    ap.add_argument("--config", default="gates.json")
    ap.add_argument("--lock", default=LOCK)
    args = ap.parse_args()
    if args.mode == "seal":
        seal(args.config, args.lock)
        return 0
    failures = check(args.config, args.lock)
    for gid, reason in failures:
        print(f"BLOCK {gid}: {reason}")
    if failures:
        print(f"{len(failures)} gate(s) failed. Promotion blocked.")
        return 1
    print("all gates fresh and unchanged")
    return 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

The exit code is the interface. 0 promotes, 1 blocks. Nothing in between.

Step 3 — Seal, then check

Record the evidence, then pin it. Two commands.

mkdir -p artifacts
python -m pytest -q > artifacts/pytest.txt
echo "smoke ok $(date -u +%FT%TZ)" > artifacts/smoke.txt
python gatecheck.py seal
python gatecheck.py check
echo "exit=$?"
Enter fullscreen mode Exit fullscreen mode

Ask any CI system one question at the promote step: did the runner exit zero? Nothing else needs to know about the checklist.

- run: python gatecheck.py check
Enter fullscreen mode Exit fullscreen mode

Step 4 — Keep a failing fixture, not a passing demo

A demo that passes proves nothing. I keep a stale artifact on purpose.

touch -d "2026-08-20 09:00" artifacts/pytest.txt
python gatecheck.py check; echo "exit=$?"
# BLOCK unit-tests: STALE 26.2d > 3d
# 1 gate(s) failed. Promotion blocked.
# exit=1
Enter fullscreen mode Exit fullscreen mode

That output is the whole test plan. If this command ever prints exit=0, my CI wiring is broken, not the code.

Where a free tier actually helps here

Here is the honest constraint. A gate runner is cheap, but the canary it guards is not, and canary runs are exactly what you skip when a call costs money.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The operator states that MonkeyCode is an open-source project offering free model access and a free server option. Those two things fit this workflow in a narrow way: I can run the gate runner and a low-stakes prompt canary on the free path, so the cheapest possible check stops being the one I skip. The outreach material also cites a free token allowance, reported as 10 million — I have not verified that number, its expiry, or any rate limits, and those terms can change. Check the project's current terms yourself before you build a process on top of them. I also cannot vouch for hardware, uptime, or permanence, because I have not measured them.

Use it for a canary. Do not use it as your only promotion gate.

Limitations and who should skip this

This approach needs machine-checkable artifacts. If your "evidence" is a meeting, it will not fit.

Skip it if you are inside a regulated audit pipeline with signed attestations. Buy that, do not script it.

Skip it if you cannot name an owner per gate. Unowned gates are decoration.

It is also deliberately blunt. There is no partial credit, no severity score, and no waiver flow. I added waivers once and they became the default path.

Cost boundary and exit criteria

Gate Refresh cost Failure it catches
unit-tests ~90s broken logic
smoke-cli ~60s entrypoint and packaging drift
cost-run ~5 min runaway spend per run
rollback-drill ~15 min promote you cannot undo

The build itself took about 40 minutes. Checks run in under a second.

Abandon the gate if you spend more than 15 minutes a week refreshing it by hand. Abandon the list if a third of the gates are stale at every check — the list is too big, not the week too short.

Rollback is simple: delete gates.lock.json and the runner, and your old checklist still works. That is the exit I want before I trust any new process.

My next iteration is the awkward gate: an artifact I can produce but cannot refresh on a schedule, like a third-party compatibility check. Do you keep those in the blocking list, or move them to a manual review step with its own expiry? That answer changes what I build next.

Top comments (0)