# The overnight queue rules and the evening preflight check (free sample)

From the Hermes Overnight Worker by PaperChaseWebb. Free to use. Works with Grok Bot, Claude Code, Codex and Hermes.

Two pieces of the worker you can use tonight, before you set up any Blender automation: the approval rules that decide what an unattended agent may do while you sleep, and a small read-only Python check that tells you whether tonight's queue follows them. No Blender needed for either.

## 1. The rules for an unattended night

1. **The queue is approved before the run.** The owner sees every job, its budgets and its source, and replies `yes queue <run_id>`. The yes names the run id. A job added after the yes means a new run id and a new yes.
2. **Every job has three budgets.** A triangle cap for the exported mesh, a wall-clock cap in minutes for the Blender process, and a cap of one to three critique rounds before the job stops and goes to a person.
3. **Writes go to staging only.** Every output goes to staging/<run_id>/<job_id>/. The owner's files sit in originals/, are hashed before the run, and are never written.
4. **The critic is not the builder.** A separate session, under its own name, judges each preview against the brief and writes keep, revise or drop.
5. **Nothing is published or bought.** No publishing, uploading, buying assets or credits, installing add-ons, deleting, or calling a paid generator. Those are daytime tasks with the owner present.
6. **The morning promote step needs the owner's yes.** One yes per job, given after the run ended. Nothing moves into the real project before that, and nothing in the project is overwritten.

Silence is not a yes. "Looks good" is not a yes for a numbered list; ask which numbers.

## 2. The folder and the queue

```
worker/
  worker.json      owner, folder names, allowed asset sources and licenses, night_minutes_max
  queue.json       tonight's run_id and jobs
  approvals.csv    kind,ref,by,answer,at,note
  originals/       read, never written
  staging/<run_id>/<job_id>/   every output
```

worker.json:

```json
{"owner": "dana", "staging": "staging", "originals": "originals",
 "allowed_sources": ["poly haven", "ambientcg"], "allowed_licenses": ["cc0"], "night_minutes_max": 420}
```

queue.json, with one good job and one that breaks four rules:

```json
{"run_id": "r2026-11-10", "jobs": [
 {"id": "j01", "name": "crate", "brief": "Produce crate, 0.6 m wide", "source": "new",
  "output_dir": "staging/r2026-11-10/j01", "budget": {"tris_max": 800, "minutes_max": 30, "rounds_max": 3},
  "publish": false, "upload": false, "purchase": false, "install_addon": false, "delete": false, "spend_usd": 0},
 {"id": "j02", "name": "lantern", "brief": "Paper lantern, match the ref", "source": "~/game/refs/lantern.png",
  "output_dir": "../exports/j02", "budget": {"tris_max": 3000, "minutes_max": 30, "rounds_max": 6},
  "publish": false, "upload": true, "purchase": false, "install_addon": false, "delete": false, "spend_usd": 0}
]}
```

approvals.csv starts with only its header row: `kind,ref,by,answer,at,note`

Budgets that work as a start for game props: 300 to 1,500 triangles for small props, 3,000 to 8,000 for a stall or kiosk, 10 to 30 minutes per scripted job, and a night budget that ends before anyone is up.

## 3. queue_preflight.py

Read-only, standard library, Python 3.8+. It checks the six rules that can be checked before anything is built:

| Check | Rule |
|---|---|
| P1 | the owner's queue yes exists for this run id |
| P2 | job ids are unique |
| P3 | every output folder is inside staging/<run_id>/ |
| P4 | every source is new or inside originals/ |
| P5 | every job has tris_max, minutes_max and rounds_max, with rounds 1 to 3 |
| P6 | no job may publish, upload, purchase, install add-ons, delete or spend |

If the jobs add up to more minutes than the night budget, it prints a NOTE. That is a warning, not a finding, since skipping late jobs is allowed.

```python
#!/usr/bin/env python3
"""queue_preflight.py: check tonight's queue before the run. Read-only, standard library, Python 3.8+.

Usage: python3 queue_preflight.py WORKER_FOLDER   (needs worker.json, queue.json, approvals.csv)
Exit 0 ready to run, 1 findings, 2 missing or unreadable file. A NOTE line is a warning, not a finding.
"""
import csv, json, os, sys

FORBIDDEN = ("publish", "upload", "purchase", "install_addon", "delete")


def main():
    if len(sys.argv) != 2:
        print(__doc__); sys.exit(2)
    W = os.path.realpath(sys.argv[1])
    try:
        cfg = json.load(open(os.path.join(W, "worker.json"), encoding="utf-8"))
        queue = json.load(open(os.path.join(W, "queue.json"), encoding="utf-8"))
        approvals = list(csv.DictReader(open(os.path.join(W, "approvals.csv"), newline="", encoding="utf-8")))
    except (OSError, ValueError) as e:
        print("ERROR: %s" % e); sys.exit(2)
    owner, run_id = cfg.get("owner", "").strip().lower(), queue.get("run_id", "")
    stage = os.path.realpath(os.path.join(W, cfg.get("staging", "staging"), run_id))
    orig = os.path.realpath(os.path.join(W, cfg.get("originals", "originals")))
    out = []
    if not any(a.get("kind") == "queue" and a.get("ref") == run_id and a.get("by", "").strip().lower() == owner
               and a.get("answer", "").strip().lower() == "yes" for a in approvals):
        out.append(("P1", run_id, "no queue yes from %s for this run id" % owner))
    ids = [j.get("id") for j in queue.get("jobs", [])]
    for jid in sorted({i for i in ids if ids.count(i) > 1}):
        out.append(("P2", jid, "job id used %d times" % ids.count(jid)))
    for j in queue.get("jobs", []):
        jid, od = j.get("id"), j.get("output_dir", "")
        p = os.path.realpath(os.path.join(W, od))
        if os.path.isabs(od) or not p.startswith(stage + os.sep):
            out.append(("P3", jid, "output_dir %r is outside staging/%s/" % (od, run_id)))
        src = j.get("source", "new")
        if src != "new" and (os.path.isabs(src) or not os.path.realpath(os.path.join(W, src)).startswith(orig + os.sep)):
            out.append(("P4", jid, "source %r is not inside originals/" % src))
        b = j.get("budget", {})
        miss = [k for k in ("tris_max", "minutes_max", "rounds_max") if not isinstance(b.get(k), int) or b[k] <= 0]
        if miss:
            out.append(("P5", jid, "budget missing or not a positive number: " + ", ".join(miss)))
        elif b["rounds_max"] > 3:
            out.append(("P5", jid, "rounds_max %d is over the stop rule of 3" % b["rounds_max"]))
        bad = [k for k in FORBIDDEN if j.get(k)] + (["spend_usd=%s" % j["spend_usd"]] if (j.get("spend_usd") or 0) > 0 else [])
        if bad:
            out.append(("P6", jid, "overnight job asks for: " + ", ".join(bad)))
    total = sum(j.get("budget", {}).get("minutes_max", 0) or 0 for j in queue.get("jobs", []))
    if total > cfg.get("night_minutes_max", 420):
        print("NOTE %s jobs add up to %d minutes, night budget %d; later jobs may be skipped" % (run_id, total, cfg.get("night_minutes_max", 420)))
    for code, ref, msg in out:
        print("%s %-12s %s" % (code, ref, msg))
    print("%d finding(s)%s" % (len(out), "" if out else ": ready for the night"))
    sys.exit(1 if out else 0)


if __name__ == "__main__":
    main()
```

## 4. Test it

Save the three files above into a folder named W, then:

```
python3 queue_preflight.py W
```

Expected output (exit 1):

```
P1 r2026-11-10  no queue yes from dana for this run id
P3 j02          output_dir '../exports/j02' is outside staging/r2026-11-10/
P4 j02          source '~/game/refs/lantern.png' is not inside originals/
P5 j02          rounds_max 6 is over the stop rule of 3
P6 j02          overnight job asks for: upload
5 finding(s)
```

Fix j02: source `originals/refs/lantern.png`, output_dir `staging/r2026-11-10/j02`, rounds_max 3, upload false. Then add the owner's yes to approvals.csv:

```
queue,r2026-11-10,dana,yes,2026-11-10T21:15:00,evening queue
```

Run it again. Expected (exit 0):

```
0 finding(s): ready for the night
```

We ran both steps on 2026-10-28 and got the output above. A missing file gives exit 2.

## What the full pack adds

The Hermes Overnight Worker ($29) is the whole night, not just the evening half:

- build_prop.py: one headless Blender job that refuses outside staging, enforces the triangle cap, and writes a .glb, a work .blend, a preview and a result file
- night.py: the runner. It refuses without the queue yes, hashes originals, stops each job at its minutes budget, refuses a critic who is also the builder, and runs the promote step only when the check is clean, never overwriting a file
- queue_check.py: fifteen read-only checks on the finished night, including originals untouched, outputs matching their hashes, and promote yes given after the run ended
- A seeded fixture with fifteen planted faults, the fifteen fixes, and the expected output (15 findings, then 0)
- A real-Blender demo night with its expected output, tested on Blender 5.2.3
- Four skills (queue, night builder, critic, morning report), ten prompts, a rules block, setup notes for Grok Bot, Claude Code, Codex and Hermes, an install plan and a failure table

If you'd rather have it set up for you, the Install sprint ($5,000) sets up the worker on your machine and your project, and Manage ($5,000/mo) runs it with you. Book a call at paperchasewebb.ai.

## Sources

- Hermes Agent, Skills: https://hermes-agent.nousresearch.com/docs/user-guide/features/skills
- Hermes Agent, Scheduled tasks (cron): https://hermes-agent.nousresearch.com/docs/user-guide/features/cron
- Blender manual, command line arguments: https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html

PaperChaseWebb 💸
