I keep almost leaving a worker running overnight.
The CLI still talks to a remote model.
My laptop lid is already halfway closed tonight.
What actually proves that worker should keep going?
A green sample is not a real gate.
I wanted one file that can fail closed.
The file has to work when I am away.
The Friday constraint
The worker does exactly one narrow task.
It classifies a folder of plain notes.
Then it writes JSON lines onto disk.
I will not babysit that loop tonight.
I will not pay for a retry storm.
If the remote path dies, a local stub takes over.
Time box sits at forty-five minutes total.
Token box is free model access only.
Abandon after three failed probes, then stop.
Want a copyable artifact instead of vibes?
Use a JSON gate file plus a tiny runner.
Those two files are the whole launch ritual.
What this checklist is not
This is not another model bake-off.
This is not an agent framework review.
This is a ship-or-sleep checklist for one CLI.
If a gate cannot print evidence, it fails.
If a required gate is missing, nothing starts.
The runner stays fail closed on purpose.
Eight gates I refuse to skip
I keep these eight lines on a card.
Then I copy them into gates.json.
The runner is the only voice that may say go.
- Confirm the heartbeat path is writable.
- Pin max wall clock as a positive integer.
- Pin max remote calls as a hard cap.
- Treat empty model output as a hard abort.
- Keep output files inside a short allowlist.
- Honor a
STOPfile on the next tick. - Require a local stub command on PATH.
- Refuse to start under a disk free floor.
Eight gates are enough for one task.
A ninth gate turns into a manifesto.
I do not need a manifesto at 6 p.m.
Step 1 — Write the gate file
Save this JSON next to your worker CLI.
Read it twice before you change any values.
Notice there is no retry-forever key at all.
{
"task": "classify-notes",
"heartbeat_path": "./run/heartbeat.txt",
"max_wall_seconds": 3600,
"max_remote_calls": 40,
"empty_output_policy": "fail_closed",
"output_allowlist": ["./out/", "./run/"],
"stop_file": "./run/STOP",
"stub_command": ["python", "stub_classify.py"],
"min_free_mb": 512,
"probe_attempts": 3,
"probe_timeout_seconds": 8
}
That missing key is the whole design.
Overnight workers should not invent new retries.
Caps belong in a file, not in my memory.
Step 2 — Run the go/no-go checker
The runner never calls the remote model.
It only answers go or no-go for launch.
Exit code 0 means start. Code 2 means wait.
#!/usr/bin/env python3
"""Unattended go/no-go gates. Copy, pin, and refuse defaults."""
from __future__ import annotations
import json
import shutil
import sys
import time
from pathlib import Path
def fail(msg: str) -> None:
print(f"NO-GO: {msg}", file=sys.stderr)
sys.exit(2)
def main() -> None:
cfg = json.loads(Path("gates.json").read_text())
Path("./run").mkdir(exist_ok=True)
hb = Path(cfg["heartbeat_path"])
try:
hb.write_text(str(time.time()))
except OSError as exc:
fail(f"heartbeat not writable: {exc}")
if int(cfg["max_wall_seconds"]) <= 0:
fail("max_wall_seconds must be positive")
if int(cfg["max_remote_calls"]) <= 0:
fail("max_remote_calls must be positive")
if cfg.get("empty_output_policy") != "fail_closed":
fail("empty output must fail closed")
stop = Path(cfg["stop_file"])
if stop.exists():
fail(f"stop file present: {stop}")
for folder in cfg["output_allowlist"]:
Path(folder).mkdir(parents=True, exist_ok=True)
stub = cfg["stub_command"]
if stub[0] not in {"python", "python3"} and shutil.which(stub[0]) is None:
fail(f"stub command missing: {stub[0]}")
free_mb = shutil.disk_usage(".").free // (1024 * 1024)
if free_mb < int(cfg["min_free_mb"]):
fail(f"disk free {free_mb}MB below floor")
print("GO")
sys.exit(0)
if __name__ == "__main__":
main()
Is the script glamorous automation? Not even close.
Does it block a bad unattended night? Yes, it does.
I want boring software when I leave the keyboard.
Run it before every unattended start like this:
python go_nogo.py && python worker.py
echo $?
If you see NO-GO, you do not start.
You also do not say "just this once".
That phrase is how tiny disks fill up.
Step 3 — Keep a failure fixture
I want a fixture that must fail closed.
A blog sentence is not evidence enough.
A zero-byte batch file is evidence.
mkdir -p fixtures out run
: > fixtures/empty_batch.jsonl
python - <<'PY'
from pathlib import Path
p = Path("fixtures/empty_batch.jsonl")
if p.stat().st_size == 0:
raise SystemExit("FAIL-CLOSED: empty batch")
print("unexpected non-empty fixture")
PY
The live worker should match that fixture.
No record. No fake success with zero rows.
Zero rows after a live call is an incident.
Wire the same rule inside the writer:
def write_batch(path: str, rows: list[dict]) -> None:
if not rows:
raise RuntimeError("empty output: fail closed")
target = Path(path)
if not str(target).startswith(("out/", "run/", "./out/", "./run/")):
raise RuntimeError("path outside allowlist")
target.write_text("\n".join(json.dumps(r) for r in rows) + "\n")
Would you ship a classifier that celebrates silence?
I would not ship that classifier tonight.
Silence is how a night of notes disappears.
Step 4 — Probe the remote path, then quit
The gate file sets probe_attempts to three.
That number is the abandonment criterion.
Do not raise it until the error feels nicer.
import subprocess
import sys
def fail(msg: str) -> None:
print(f"NO-GO: {msg}", file=sys.stderr)
sys.exit(2)
def probe_remote(cmd: list[str], attempts: int, timeout: int) -> None:
last = None
for _ in range(attempts):
try:
subprocess.run(cmd, check=True, timeout=timeout)
return
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
last = exc
fail(f"remote probe failed {attempts} times: {last}")
Call probes only after go_nogo.py exits zero.
Do not probe from a half-configured tree.
Order is part of the checklist, not a footnote.
A free remote box helps this step.
I do not want my laptop as the overnight host.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source project with free model access.
It also offers a free server option for small runs.
I treat that pair as a box for these gates, and it is one place to replay the same go/no-go file.
If the three probes fail, run stub_classify.py.
The stub uses local heuristics and nothing else.
It never pretends to be the remote model.
# stub_classify.py — local, boring, honest
import json
import sys
from pathlib import Path
for line in Path(sys.argv[1]).read_text().splitlines():
note = line.strip()
if not note:
continue
label = "todo" if "todo" in note.lower() else "other"
print(json.dumps({"text": note, "label": label, "source": "stub"}))
Boring output is a feature here, not a bug.
Overnight workers should stay easy to explain.
If I cannot explain a tick, I should not start.
Step 5 — Tick with a kill switch
Keep the unattended loop short on purpose.
Each tick must refresh the heartbeat file.
Each tick must honor STOP before any call.
import time
from pathlib import Path
def run_ticks(max_calls: int, wall: int, stop: Path, hb: Path) -> None:
start = time.time()
calls = 0
while calls < max_calls:
if stop.exists():
raise SystemExit("STOP file: abort")
if time.time() - start > wall:
raise SystemExit("wall clock: abort")
hb.write_text(str(time.time()))
# one remote or stub call lives here
calls += 1
time.sleep(1)
Why a STOP file instead of a Ctrl-C?
I will not be sitting at that keyboard.
SSH sessions drop, and memory is not a plan.
Abort from any other shell with this:
touch ./run/STOP
The next tick should die without debate.
If the loop ignores STOP, that is a bug.
File it. Do not tune the bug into a retry.
Time, cost, and rollback
Copy the files in under forty-five minutes.
Spend zero paid tokens on the first night.
Stop after one hour or forty remote calls.
Rollback is the stub command in gates.json.
If the remote path is gone, run that stub.
If the stub itself is missing, do not start.
Abandon the whole worker when any of these hit:
- three remote probes fail in a row
- the heartbeat file goes stale on disk
- output appears outside the allowlist
- free disk drops under 512MB
That last gate has bitten me on tiny boxes.
Logs feel free until the volume is full.
The checklist exists to make that failure loud.
Who should not copy this
Skip this if you run a multi-tenant mesh.
Skip this if you need formal SRE coverage.
Skip this when the task is not one-shot.
This checklist is for a solo builder.
It fits a one-task CLI and a short night.
It does not replace a real platform team.
Do not point the worker at secrets.
Do not point it at production writes.
Do not let it create files outside out/ and run/.
Limits I am still holding
I have a gate for empty output today.
I do not have a gate for plausible junk.
Those two failures are not the same bug.
A remote model can return confident nonsense.
The JSON may parse without a single error.
The labels can still be completely wrong.
That is the next fail-closed check I want.
I do not want a bigger agent framework.
I want one more boring gate in the file.
If you try this on a free remote box, start with the empty fixture.
Then touch STOP while a tick is still running.
Then force the disk floor in a throwaway folder.
Did the worker halt on each of those?
If it kept going, the checklist lied to you.
Which unattended gate failed first on your box: empty output, a stale heartbeat, or a STOP file the loop ignored?
Top comments (0)