After-hours coding agents fail at handoff, not generation. Weekend queues leave unowned diffs and invented context. A one-page wiki card with named roles stops that drift.
Cheap night agents resemble forklifts idling in a dark warehouse. The machine still moves without a licensed driver. Morning crews then inherit bent pallets and a hot motor.
Free-tier boxes make the problem quieter, not smaller. The process keeps writing while chat stays silent. Missing files become confident comments inside the patch.
Agentic helpers now sit on small servers beside the repo. They draft tests, rename modules, and open branches. They also invent callers, configs, and flags that never existed.
That habit is the real overnight failure mode. Cheap generation is not the overnight failure. Unowned assumptions remain the true overnight failure.
This SOP treats the night as a staffed shift. It assigns three human names before the worker starts. It freezes the queue when any name is absent.
The Queue Owner holds the pager for the agent process. That person starts the worker and signs the stop time. Nobody else may restart the job after freeze.
The Context Steward owns every path the agent may touch. Guessed imports and phantom configs fail this check. Unsigned assumed paths mean the worker must halt.
The Freeze Operator runs a clock, not a model. A fifteen-minute job reads the night card. A stale or unsigned card kills the worker.
Think of the card as a dock manifest, not a prompt. A warehouse will not move cargo without a signed sheet. The agent should not move code without one either.
Handoff is a file write, not a hallway story. Thursday posts the blank card in the wiki. Friday afternoon fills names, paths, and the stop clock.
The next owner should finish the card in one minute. Anything longer means the night was not staffed. Staffing is the control, not the model.
Paste this block into the team wiki before the first weekend run. Keep it on one screen. Do not grow it into a novel.
# Night Queue Card
- Shift date:
- Queue Owner (pager):
- Context Steward (paths):
- Freeze Operator (clock):
- Stop after (ISO-8601 UTC):
- Allowed paths:
- Forbidden paths:
- Assumed context (empty or signed):
- Signed by:
- Morning owner:
- Resume command:
- Freeze rule: halt if this card is stale > 15 minutes
The wiki card is still theater without a local freeze loop. A short script can enforce it on any small box. The listing below is a template for a dry run, not a shipped service.
Store the live shift as tonight.json beside the repo. Humans edit JSON. Cron only reads it.
{
"queue_owner": "alex.r",
"context_steward": "sam.k",
"freeze_operator": "jordan.p",
"stop_after": "2026-09-06T02:00:00+00:00",
"allowed_paths": ["src/queue/", "tests/queue/"],
"forbidden_paths": [".github/", "infra/"],
"assumed_context": [],
"assumed_signed_by": ""
}
The watcher loads that card, then checks the worker pid. It also scans git diff against allowed paths. A path escape freezes the queue with a morning note.
#!/usr/bin/env python3
"""Night-queue freeze template. Dry-run on an empty repo first."""
import argparse, json, os, signal, subprocess, sys
from datetime import datetime, timezone
from pathlib import Path
REQUIRED = (
"queue_owner", "context_steward", "freeze_operator",
"stop_after", "allowed_paths",
)
def load_card(path):
data = json.loads(Path(path).read_text())
missing = [k for k in REQUIRED if not data.get(k)]
if missing:
raise SystemExit(f"incomplete card: {missing}")
assumed = data.get("assumed_context") or []
if assumed and not data.get("assumed_signed_by"):
raise SystemExit("assumed_context present but unsigned")
stop = datetime.fromisoformat(data["stop_after"].replace("Z", "+00:00"))
if datetime.now(timezone.utc) >= stop:
raise SystemExit("stop_after has passed")
return data
def pid_alive(pidfile):
p = Path(pidfile)
if not p.exists():
return None
pid = int(p.read_text().strip())
try:
os.kill(pid, 0)
return pid
except OSError:
return None
def write_morning(path, reason, pid):
Path(path).write_text(
"# Morning handoff\n\n"
f"- freeze_reason: {reason}\n"
f"- freeze_time: {datetime.now(timezone.utc).isoformat()}\n"
f"- pid_was: {pid}\n"
)
def freeze(pidfile, reason, morning):
pid = pid_alive(pidfile)
if pid:
os.kill(pid, signal.SIGTERM)
write_morning(morning, reason, pid)
print(reason)
sys.exit(2)
def scan_diff(allowed, forbidden):
out = subprocess.check_output(
["git", "diff", "--name-only", "HEAD"], text=True
)
bad = []
for line in out.splitlines():
if not line:
continue
if any(line == f or line.startswith(f.rstrip("/") + "/") for f in forbidden):
bad.append(line)
continue
ok = any(line == a or line.startswith(a.rstrip("/") + "/") for a in allowed)
if not ok:
bad.append(line)
return bad
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--card", required=True)
ap.add_argument("--pidfile", required=True)
ap.add_argument("--morning", default="morning.md")
args = ap.parse_args()
try:
card = load_card(args.card)
except SystemExit as e:
freeze(args.pidfile, str(e), args.morning)
return
bad = scan_diff(card["allowed_paths"], card.get("forbidden_paths") or [])
if bad:
freeze(args.pidfile, f"path escape: {bad}", args.morning)
return
print("card ok")
if __name__ == "__main__":
main()
A Friday drill proves the card before a real weekend. Use an empty worktree for that drill. No production branch should receive the test patches.
Label this drill as a synthetic run. It is not a production incident report. The point is the freeze path, not a score.
The Queue Owner writes tonight.json at 17:45 local time. The Context Steward lists every path the agent may edit. The Freeze Operator starts the watcher and leaves the desk.
chmod +x freeze_queue.py
echo $$ > /tmp/agent.pid
# stand-in worker for the drill; replace with the real agent pid
python3 freeze_queue.py --card tonight.json --pidfile /tmp/agent.pid --morning morning.md
Cron should call that command every fifteen minutes. A laptop lid is not a clock. The operator owns the timer, not the author of the prompt.
*/15 * * * * cd /srv/night-queue && python3 freeze_queue.py --card tonight.json --pidfile /tmp/agent.pid --morning morning.md >> /var/log/night-queue.log 2>&1
At the first tick the watcher should find a complete card. A missing signature must stop the worker immediately. The morning file should name the human who resumes.
If the agent writes outside allowed paths, the steward fails the shift. The freeze operator does not debate the model. The kill is the entire decision.
Assumption text belongs on the card, never in a commit message. Phrases like probably, should exist, and assume are smoke. A tiny scan can fail the shift on those words.
git diff HEAD | grep -Ei 'probably|should exist|assume[sd]? that' && echo FAIL_ASSUMPTION
That grep is rude and useful. It does not prove correctness. It only stops the forklift when the aisle is unlabeled.
Morning handoff is a file, not a standup story. morning.md lists freeze time, last pid, and the reason string. The daytime owner reads that file before any merge.
git log -1 --oneline
test -f morning.md && cat morning.md
git diff --stat HEAD
Resume is a named command on the wiki card. The daytime owner runs only that command. Ad-lib restarts recreate the dark warehouse.
Some teams host the watcher on the same cheap host as the agent. MonkeyCode is an open-source coding assistant with free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The product matters here only as a box for the worker and the watchdog. This article does not claim quotas, model names, or uptime. The wiki card still works if that host is replaced.
Keep the agent and the freeze loop as two processes. One pid writes code. One pid reads the card. A single blended process hides the kill switch.
Limitations stay sharp on purpose. This playbook does not measure model quality. It does not replace code review, tests, or CI.
It will not catch a well-formed but wrong refactor. It will not meter token spend by itself. It only answers who owns the night queue.
Stale cards fail closed, which can annoy a late owner. That annoyance is cheaper than a Monday surprise branch. Tune the fifteen-minute window only after a clean drill.
Git path checks follow prefix rules, not semantic intent. A renamed file can look like a path escape. The steward then updates the card in daylight, not at midnight.
Teams with paid twenty-four-hour on-call can skip this card. Solo hobby repos do not need three named humans. Regulated codebases need change control stronger than a JSON file.
Do not point this watcher at a shared production database. Do not let the agent hold cloud credentials overnight. Do not treat an empty assumed-context field as proof of truth.
Do not run the drill on a laptop that sleeps. Do not share one Queue Owner name across two repos. Do not paste secrets into tonight.json.
The useful outcome is a boring Monday. The queue is stopped or named. The diffs have a steward.
If a team already wants a cheap box for the watchdog, the free server option is enough to rehearse the night card. Paste the wiki block first, then run the freeze script against an empty queue.
Top comments (0)