# Planner Over Cheap Models, free sample: the planner and worker contract plus a preflight check

Version 1.0, 2026-10-28. By PaperChaseWebb. Works with Grok Bot, Claude Code, Codex and Hermes. Free. One Python 3 script, standard library only, tested on the worked example below.

This is the part of Planner Over Cheap Models (pp-planner-over-cheap-models, $24) you can use today: the six rules that let a strong model plan while cheap or local models write the code, and plan_check.py, which reads your plan before any worker starts and says what will go wrong. The full pack adds the logger that runs each check itself, a fourteen-check read-only audit of the finished run, a seeded fixture, the owner approval steps, the worker bake-off, ten prompts and setup for all four harnesses.

## The six rules

1. **The planner plans and reviews. It never edits.** Give the planner session read and plan tools only. If it can write, it will, and the expensive model ends up doing the cheap work.
2. **Every task has a check command, written before the worker starts.** A command that exits 0 on pass. No check, no task. A worker's "done" is a claim; the check is the result.
3. **The check runs, not the worker.** You or a script run the task's check after the worker's turn and record the exit code. Never take the exit code from the worker's message. Cheap models do report success on work that failed.
4. **Tasks that can run at the same time own different files.** If two tasks need the same file, one depends on the other.
5. **High risk goes up a tier or to a person.** Auth, money, deletes, migrations and anything public run on the strong tier or get a human review.
6. **The owner says yes to the plan before any worker starts.** Plain words with the run id, recorded with the time. Silence is not a yes.

## A task card

```json
{"id": "T2", "goal": "render a recipe card",
 "files": ["src/render.js", "test/render.test.js"],
 "check": "node test/render.test.js",
 "risk": "low", "tier": "local", "review": "planner",
 "depends_on": ["T1"]}
```

tier is strong, cheap or local. review is planner or human. A worker sees only its card and the files on it.

## plan_check.py

Save as plan_check.py. It reads route.json and plan.json in a run folder and writes nothing. Exit 0 means no findings, 1 means findings, 2 means a file is missing.

| Code | Finding |
|---|---|
| P1 | planner not on the strong tier or can edit, write or run shell; or a worker not on cheap or local |
| P2 | a task has no check command |
| P3 | depends_on names a task that does not exist, or the tasks form a loop |
| P4 | two tasks that can run in parallel own the same file |
| P5 | a high-risk task on a non-strong tier with no human review |

```python
#!/usr/bin/env python3
"""plan_check.py: read-only preflight on route.json and plan.json before any worker starts.

Usage: python3 plan_check.py RUN_DIR
Exit 0 = no findings, 1 = findings, 2 = file missing. Writes nothing. Standard library only.
PaperChaseWebb, version 1.0, 2026-10-28.
"""
import json, os, sys

def main():
    if len(sys.argv) != 2: print("usage: plan_check.py RUN_DIR"); sys.exit(2)
    run = sys.argv[1]
    for n in ("route.json", "plan.json"):
        if not os.path.isfile(os.path.join(run, n)): print("ERROR: missing", n); sys.exit(2)
    route = json.load(open(os.path.join(run, "route.json")))
    tasks = {t["id"]: t for t in json.load(open(os.path.join(run, "plan.json"))).get("tasks", [])}
    out = []
    # P1 planner strong and read-only, workers cheap or local
    for name, r in route.get("roles", {}).items():
        if r.get("kind") == "planner" and (r.get("tier") != "strong" or {"edit", "write", "shell"} & set(r.get("tools", []))):
            out.append(f"P1 planner {name} must be on the strong tier with no edit, write or shell tools")
        if r.get("kind") == "worker" and r.get("tier") not in ("cheap", "local"):
            out.append(f"P1 worker {name} is on tier {r.get('tier')!r}; use cheap or local, escalate with a yes")
    # P2 every task has a check command
    for tid, t in tasks.items():
        if not str(t.get("check", "")).strip(): out.append(f"P2 task {tid} has no check command")
    # P3 depends_on names real tasks and has no loop
    for tid, t in tasks.items():
        for d in t.get("depends_on", []):
            if d not in tasks: out.append(f"P3 task {tid} depends on unknown task {d}")
    def loops(a):
        stack, seen = list(tasks[a].get("depends_on", [])), set()
        while stack:
            d = stack.pop()
            if d == a: return True
            if d in tasks and d not in seen: seen.add(d); stack.extend(tasks[d].get("depends_on", []))
        return False
    for tid in sorted(tasks):
        if loops(tid): out.append(f"P3 task {tid} is part of a dependency loop")
    # P4 parallel tasks own different files
    def reach(a, b):
        stack, seen = list(tasks[a].get("depends_on", [])), set()
        while stack:
            d = stack.pop()
            if d == b: return True
            if d in tasks and d not in seen: seen.add(d); stack.extend(tasks[d].get("depends_on", []))
        return False
    ids = sorted(tasks)
    for i, a in enumerate(ids):
        for b in ids[i + 1:]:
            both = sorted(set(tasks[a].get("files", [])) & set(tasks[b].get("files", [])))
            if both and not reach(a, b) and not reach(b, a): out.append(f"P4 tasks {a} and {b} can run in parallel and both own {both}")
    # P5 high risk needs a strong worker or a human review
    for tid, t in tasks.items():
        if t.get("risk") == "high" and t.get("tier") != "strong" and t.get("review") != "human":
            out.append(f"P5 task {tid} is high risk on tier {t.get('tier')!r}; set tier strong or review human")
    print(f"plan_check {run}: {len(out)} finding(s)")
    for line in out: print(" ", line)
    sys.exit(1 if out else 0)

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

## Worked example: 5 findings, then 0

Make a folder ex/ with these two files.

route.json:

```json
{"owner": "owner", "roles": {
 "planner": {"kind": "planner", "model": "strong-planner", "tier": "strong", "tools": ["read", "plan", "edit"]},
 "worker":  {"kind": "worker",  "model": "local-coder-a",  "tier": "local",  "tools": ["read", "edit", "shell"]}}}
```

plan.json:

```json
{"tasks": [
 {"id": "T1", "goal": "parse CSV rows", "files": ["src/parse.js"], "check": "node test/parse.test.js", "risk": "low", "tier": "local", "review": "planner", "depends_on": []},
 {"id": "T2", "goal": "render a card", "files": ["src/render.js", "src/parse.js"], "check": "", "risk": "low", "tier": "local", "review": "planner", "depends_on": ["T9"]},
 {"id": "T3", "goal": "check the upload token", "files": ["src/auth.js"], "check": "node test/auth.test.js", "risk": "high", "tier": "local", "review": "planner", "depends_on": []}]}
```

Run `python3 plan_check.py ex`:

```
plan_check ex: 5 finding(s)
  P1 planner planner must be on the strong tier with no edit, write or shell tools
  P2 task T2 has no check command
  P3 task T2 depends on unknown task T9
  P4 tasks T1 and T2 can run in parallel and both own ['src/parse.js']
  P5 task T3 is high risk on tier 'local'; set tier strong or review human
```

Four fixes:

- remove "edit" from the planner's tools (P1)
- give T2 the check `node test/render.test.js` (P2)
- change T2's depends_on from T9 to T1; T2 now runs after T1, which also clears the shared parse.js (P3 and P4)
- set T3's review to human (P5)

Run it again:

```
plan_check ex: 0 finding(s)
```

We ran this on 2026-10-28 with Python 3 and got 5 findings (exit 1), then 0 (exit 0).

## What the full pack adds

- log_attempt.py, which runs each task's check itself and writes the real exit code to the ledger
- route_check.py, fourteen read-only checks on a finished run, including done claims on failing checks, files outside the card, late or missing owner yes, unapproved escalations, merges without a yes, the planner token budget, and a report that does not match the ledger
- a seeded fixture with fourteen planted faults: 14 findings, then 0 on the fixed run
- the three owner approval steps: plan, escalation and merge
- the worker bake-off: pick the cheap model by cost per passed task on your own tasks
- rules blocks, ten prompts, setup for Grok Bot, Claude Code, Codex and Hermes, an install plan and a failure table

To measure cost per solved task before you change anything, see Agent Cost Levers (pp-agent-cost-levers). If you want this installed and run for you, the Install sprint and Manage plans are on the home page at paperchasewebb.ai, where you can book a call.

Source credit: the run that showed a cheap worker can double the planner's bill is u/GapNew4766's three logged runs on r/ClaudeAI (https://www.reddit.com/r/ClaudeAI/comments/1xczls1/). Their numbers are from one setup; measure your own.

PaperChaseWebb 💸
