Models & lanes
Not every backlog item deserves the same firepower. Echo scales two things to an
item’s size and risk: which model the worker runs on, and how much review
ceremony it performs. Both decisions are made deterministically at spawn time by
spawn_worker.sh, from signals the dispatcher can actually see — the item’s
Size field, its Category, and its labels.
Pre-validate — the screen before spawn
Section titled “Pre-validate — the screen before spawn”Before a worker is ever spawned, dispatch_one.sh runs prevalidate_item.sh: a
single small-model call (workers.prevalidateModel, default haiku) that reads
the item’s title, body, labels, and Category and returns one verdict —
proceed, needs_human, or defer.
proceed— the item is dispatchable; spawn continues.needs_human— the work itself requires a missing capability (hardware / HIL, secret rotation, destructive infra, a legal/compliance decision) or is fundamentally underspecified. The item is flipped straight to Needs Human.defer— the item is blocked on an open issue or unmerged PR. It’s re-queued to the bottom of Ready with an invisible marker comment (deferMarker, default<!-- echo-defer -->). Afterworkers.deferCap(default3) defers, the next one escalates to Needs Human instead of looping.
The rubric is bail-action based, not keyword based: an item is only
needs_human when shipping its acceptance criteria would itself require the
missing capability — merely mentioning a bail term (in an Out-of-Scope list, a
feature enumeration, or a meta/docs item) reads as proceed. Pre-validate is
fail-open: no claude binary, a failed call, or an unparseable response all
resolve to proceed (the worker has its own bail gate as a backstop). Disable it
entirely with ECHO_PREVALIDATE_DISABLED=1.
Model routing — workers.modelBySize
Section titled “Model routing — workers.modelBySize”worker_model_for_size normalizes the item’s Size label to its bare token
(uppercase, first word — so "M (≈1 day)" and "xs" both resolve), then looks
up workers.modelBySize:
"workers": { "slots": 2, "prevalidateModel": "haiku", "tokenBudget": 200000, "deferCap": 3, "modelBySize": { "xs": "sonnet", "s": "sonnet", "m": "opus", "l": "opus", "xl": "opus", "default": "opus" }}An unrecognized or missing Size falls to the default key. Each size is also
env-overridable (ECHO_WORKER_MODEL_XS, …_M, etc.). Regardless of which model
implements the item, PR/code review is always pinned to a strong model —
cheapening the implementation never cheapens the review.
Risk — derived from Category and labels
Section titled “Risk — derived from Category and labels”risk_for_item returns high or normal from two config regexes:
"risk": { "categoryRegex": "auth|billing|security|money|payment|contract", "labelRegex": "^(security|auth|billing|money-path)$"}If the item’s Category matches risk.categoryRegex, or any label
matches risk.labelRegex, risk is high; otherwise normal. Both regexes are
env-overridable (ECHO_RISK_CATEGORY_RE, ECHO_RISK_LABEL_RE) so the risk
surface is tunable without code.
Lane routing — lane_for_size_risk
Section titled “Lane routing — lane_for_size_risk”lane_for_size_risk maps normalized size + risk to one of three lanes, and the
worker reads the pre-computed lane from its prompt and applies it directly:
| Size (normal risk) | Lane | Review ceremony |
|---|---|---|
| XS, S | light | single-lens, shallow verify (maxReviewers: 1, maxVerifiers: 1) |
| M (or unknown / missing) | default | full 4-lens adversarial fan-out |
| L, XL | full | full 4-lens fan-out + ultracode opt-in |
risk: high forces full regardless of size. Auth / billing / security /
money-path work never runs on the light lane. Two invariants matter:
- Brainstorming (Step 2) never scales down. The lane trims only review fan-out; every lane brainstorms fully.
- The lane is a floor, not a ceiling — escalate, never de-escalate. The
dispatcher only sees author-controllable metadata, not which files the worker
will touch. If, while planning, the worker finds its diff touches gated or
security-sensitive paths, it upgrades to
full. This is also enforced mechanically at review time: the worker computes the real changed-file set and any match against the security-sensitive patterns forcesfullso the security lens runs — closing the gap where a mislabeled small item could touch a gated path yet skip the security review.
Review caps
Section titled “Review caps”Review fan-out is hard-capped inside the workflow by the review.caps config —
maxReviewers (default 4), maxVerifiers (default 3), maxFindings
(default 12). Lane overrides only ever lower the fan-out; anything dropped is
logged, never silently truncated. review.strongModel (default opus) is the
model the review is pinned to.
"review": { "holdForHuman": true, "gateLabel": "review-block", "strongModel": "opus", "caps": { "maxReviewers": 4, "maxVerifiers": 3, "maxFindings": 12 }}See also
Section titled “See also”- The run lifecycle — pre-validate and spawn in context.
- Merge gates — why the review is advisory, not a merge gate.
- Config reference — the
workers,risk, andreviewblocks.