High-to-Low: One Frontier Model Plans and Judges, Cheap Models Do the Work

2026-09-22

Most of what a coding agent does in a run is typing, not thinking. Read the file, find the string, replace it, run the test, read the output, replace another string. Somewhere in there is a real decision: what the change should be, which files it touches, how you know it worked. That decision is worth a frontier model. The typing is not. Paying frontier rates for typing is the waste. Asking a small model to make the decision is the failure. Almost every agent setup today does both at once, because one model does the whole run.

High-to-Low (H2L) splits the jobs by model. It shipped in Omnimancer 0.3 this week, and this post is about what it is, why the workers are deliberately blind, why the judge never reads the worker's explanation, and what the first live plans taught us before a single worker ran.

One H2L run

Goal

one sentence from you

High plans

reads the repo · writes stories

Low pool executes

one story per worker · no exploring

Checks

verify exit · scope · diff

High judges

scores the diff · 0–100

Pass · retry · escalate

feedback goes to the next model

The principal engineer and the juniors

The mental model is a team, not a pipeline. A principal engineer reads the codebase, writes a short design, and breaks the work into a backlog of stories. Each story is written for a junior who has never seen the repository and is not going to look: absolute paths, the exact text to replace and what to replace it with, the exact command to run, and acceptance criteria you can check against a diff. The juniors do the work in isolation, one story each. The principal then reviews each result against the criteria they wrote, sends it back with notes if it falls short, and picks it up themselves if a junior keeps failing.

That is the whole feature. Three roles, one loop:

  1. Planner (high model). Runs with the read tools (Read, Glob, Grep) and no write tools. It records stories one at a time through an add_story tool call and closes the plan with submit_plan. Structured output arrives through tool schemas, not free-form JSON, because every tool-capable provider enforces a schema and none of them need JSON repair code.
  2. Workers (a pool of low models). You hand H2L a list, not a model. Stories are assigned round-robin across the pool, and each worker gets its own provider instance and its own conversation history. Tools are Edit, Write, Bash, plus Read limited to the files the story names.
  3. Judge (high model). Free deterministic checks first, then the high model scores the diff and the verify output against the acceptance criteria through a submit_verdict tool call. Below the threshold, the story goes back to the next model in the pool with the judge's feedback attached, up to max_retries. After that, an escalation policy decides: the high model does the story itself, the story is blocked, or (in the terminal) you are asked.

The difference from "just use subagents" is in what the workers are not allowed to do.

Why the workers are blind

A small model handed a repository and a goal wanders. It greps for the wrong thing, reads six files, hallucinates a path, and gives up or, worse, edits something plausible. The same model handed an absolute path, a verbatim old_string, a new_string, and a verify command does fine. Nobody was producing those instructions for it, so H2L makes producing them the planner's entire job and makes the worker unable to compensate for a vague one.

That is enforced, not suggested. A worker's tool list has no Glob, Grep, or WebFetch. A Read, Edit, or Write on a path outside the story's files list is refused before it reaches the approval gate, with an error the worker can see. A tool name that isn't in the allowlist is refused the same way. The worker prompt is short and reads like instructions to someone who follows steps literally: do exactly what the story says, in order; if an old_string doesn't match the file, stop and say so, do not guess.

The consequence is a clean rule for diagnosing failures: if a worker needed to search, the story was under-specified, and that is the planner's bug. Story quality is the whole game, so the planner prompt holds it to a checklist, and after the first submit_plan it gets a self-check turn: re-read every story as the worker will see it and fix any missing exact string, relative path, unquoted reference, missing depends_on, or acceptance criterion that can't be checked from a diff.

S2 · Reject webhook payloads over 1 MiB

depends_on: S1 · verify: go test ./internal/api/ -run TestWebhook

what the planner must write

files

/home/dev/app/internal/api/webhook.go
/home/dev/app/internal/api/webhook_test.go

instructions

In webhook.go, use Edit with old_string body, err := io.ReadAll(r.Body) and new_string body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)). Then in webhook_test.go, append the test function given below in full …

acceptance

• The diff wraps r.Body in http.MaxBytesReader with a 1<<20 limit and changes nothing else in webhook.go
• A new test sends a 2 MiB payload and asserts HTTP 413
• The verify command exits 0

An illustrative story in the shape the planner prompt requires: absolute paths, verbatim edit strings, full content for anything new, and criteria a reviewer can check against a diff. Not output from a real run.

Stories that touch the same file get a dependency edge, declared or implied, so two workers never edit one file at the same time. Anything still independent runs in parallel under a semaphore, and the approval prompts, which block a terminal one at a time, are serialized behind a lock and prefixed with the story and model asking ([H2L S2 · gateway:qwen3-8b]) so you always know who wants to write what.

Why the judge never reads the explanation

The LLM-as-judge literature has a consistent warning: judges reward confident, verbose output and output that looks like their own writing. A worker that says "I have implemented the change and all tests pass" is not evidence of anything. So the judge is built to be immune to it.

Deterministic stage — no model call, first hit fails the attempt

worker cancelled or errored verify exited non-zero files modified outside story scope empty diff

Model stage — the high model sees only

the story + acceptance criteria the diff, scoped to the story's files verify exit code + output the worker's narrative the worker's tool log

Verdict: a 0–100 score, passed, one entry per failed criterion, and feedback written as instructions a worker can act on with no other context. Default pass threshold is 80; a verdict marked passed with a score below it is overruled.

Two things about the retry loop are deliberate. First, the feedback is written for the same blind worker the story was: name the file, the exact text to change, what the result must look like. Second, the retry goes to the next model in the pool rather than the same one. If you gave H2L three low models, a story that one of them botches gets a different model's attempt for free, which is the cheapest form of diversity we could find.

There is one place the judge is intentionally weaker. When a story exhausts its retries and escalates to the high model, the high model's own attempt is checked deterministically only. Having the high model grade its own work adds cost without signal.

What the first live plans taught us

Everything in this section came out of running the planner alone against real repositories on a hosted Qwen model, before any worker existed. All four are fixed, with tests.

The plan was being lost to a 4096-token cap, and it looked like an empty plan. The OpenAI-family and Claude providers default max_tokens to 4096, and the non-streaming tool path never surfaced finish_reason. So when a submit_plan call with the entire backlog inside it got cut off, the loop saw a rejected, empty plan and asked again. The model responded the way a model does when its plan keeps getting "rejected": it made the plan smaller. Eleven submissions, roughly eight minutes of a sixteen-minute run, each one a worse plan than the last. The fix was structural, not a bigger number. Stories are now recorded one per add_story call and validated as they arrive, so no single response has to carry the backlog; the planner's output cap defaults to 16,384 tokens and workers' to 8,192; the provider reports finish_reason; and the loop tells the model when its output was truncated instead of letting it infer the wrong thing.

Providers that exist only through an environment variable didn't resolve. Omnimancer's env override pass materializes a provider entry from something like DIGITALOCEAN_INFERENCE_KEY, but it returns a deep copy, so the entry never lands in stored config. H2L resolved model references against stored config and reported the provider as not configured. It now resolves against the same effective config the engine initializes from, with a fallback to the live instance.

Shared-file dependencies created cycles that weren't there. Two stories touching the same file get an implicit ordering edge so they never run concurrently. The first version added that edge blindly, in plan order. When the planner had already declared the reverse dependency, the implicit edge closed a cycle and the plan was rejected for a mistake the planner hadn't made. The edge is now added only when the pair isn't already ordered either way, and a rejection reports every validation problem at once instead of the first.

The self-check turn forced a full regeneration. Asking the planner to re-read its stories and resubmit meant every plan was generated twice, even when nothing was wrong. It now replies OK if the plan stands, fixes go through add_story by id so only corrected stories change, and the phase is bounded.

One more decision came out of the same runs, and it's the one that will surprise people who have built agent loops: the planner has no iteration cap by default. Most users don't care about iterations; they care whether the plan happened. So failure messages name the real cause, a rejection or a truncation, never "iteration 20 of 20", and a cap is opt-in.

What is and isn't measured yet

We are not going to publish numbers we don't have. The loop, the worker scoping, the judge stages, the retry and escalation paths, and both surfaces are covered by 203 tests. The planner has run live against real repositories, and that is where the findings above came from. The end-to-end run, with a frontier high model, a small low model, a first-attempt pass rate, and the high-versus-low token split, is the next post. The point of the feature is to move spend from the high tier to the low tier, so the run reports usage per tier and per story, and that number is what we'll lead with when we have it.

Also not landed yet: story-level checkpoint and resume in headless mode (a killed run restarts the plan, not the next unfinished story) and per-worker activity rows in the REPL's live display. And H2L is in Omnimancer today, not yet in Factory Nexus swarms: Factory pins the Omnimancer image at a specific version and picks this up on the next bump, at which point a swarm task can run with a planner on one tier and workers on another.

Try it

H2L is in omnimancer-cli 0.3.1 on PyPI. Model references are <provider entry>:<model>, where the entry is any provider in your Omnimancer config, including an openai-compatible alias pointed at a local vLLM or Ollama server. Both tiers must support native tool calling, and a provider that doesn't is rejected at validation time with the alias workaround in the error message.

pip install "omnimancer-cli>=0.3.1"

# Headless: plan, execute across two low models, judge
omn -p "Add per-client rate limiting to the webhook handler" \
  --h2l \
  --high claude:claude-opus-5 \
  --low gateway:qwen3-8b --low digitalocean:alibaba-qwen3-32b \
  --h2l-parallel 2 \
  --output-format stream-json --dangerously-skip-permissions

# Dry run: stop after the plan, spend nothing on workers
omn -p "Add per-client rate limiting to the webhook handler" --h2l --h2l-plan-only

In the REPL, /h2l config set high claude:claude-opus-5 low gateway:qwen3-8b stores the tiers, /h2l <goal> plans and shows the stories as a table, and the plan gate asks [a]ccept [d]rop <id> [e]dit <id> [c]ancel before any worker starts. /h2l plan <goal> and /h2l run split that into two steps. The other knobs (max_parallel, max_retries, pass_threshold, escalation, worker_tools) default to 2, 2, 80, ask, and Edit/Write/Bash.

For an orchestrator, the stream-json feed gains one event type, h2l, with subtypes plan, planner_tool, waiting, story_start, story_report, story_verdict, story_blocked, story_skipped, and done; the final result carries a per-story summary and usage_by_tier. Exit codes follow the existing Omnimancer contract: 0 every story passed, 1 planning failed, 3 something was blocked or skipped, 4 rate-limited.