Skip to content

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.

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 -->). After workers.deferCap (default 3) 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.

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_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_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)LaneReview ceremony
XS, Slightsingle-lens, shallow verify (maxReviewers: 1, maxVerifiers: 1)
M (or unknown / missing)defaultfull 4-lens adversarial fan-out
L, XLfullfull 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 forces full so the security lens runs — closing the gap where a mislabeled small item could touch a gated path yet skip the security review.

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 }
}