choosing-subagent-models
Use before launching a subagent, an Agent or Task call, or an Explore agent, and on every review dispatch - code review, dual review, merge-gate reviewer. Picks the model and effort tier for the runti
SKILL.mdclaude-plugin/skills/choosing-subagent-models/SKILL.md
GitHubChoosing Subagent Models
Audit, review, or question? Require a file.
Every subagent you dispatch for an answer must WRITE ITS RESULT TO A FILE
(e.g. /tmp/<task>.md); its reply to you is only a summary. Put this
in the prompt.
Pick a TIER by role/task, then resolve it for YOUR runtime. Pin every spawn.
Model selection here is two steps, always in this order:
- Pick the semantic tier by the spawned agent's role/task — one of three,
named identically everywhere in the fleet (Go constants, role templates,
this doc):
mechanical(rote/find-replace/grep-and-collect/lint/file-maps — the cheapest tier),judgment(the default tier for implementers, reviewers, verifiers, researchers — real analysis, not busywork), andorchestration(orchestrator/coordinator tier — never spawned for a sub-agent, only ever the operator's own agent or a skill that explicitly names it for one step). - Resolve that tier to a concrete
model:+effort:for the runtime you are actually running under — via that runtime's own preset (RuntimePreset.TierModels) or operator config (runtime.role_models); see "Where the tier-model mapping lives" below. Never assume Claude's model names (sonnet,opus,haiku) apply — a Codex or OpenCode agent reading this same file has no such models, and passing one either errors or silently falls back to the runtime's own default (see "Never let an unresolved pin fall through" below).
Every subagent you spawn MUST pass an explicit, resolved model: (plus effort
where the runtime supports it). Omitting it runs the subagent on YOUR model —
the single biggest avoidable cost leak in agent work.
Haiku 5.5 is eligible for summarization and mechanical work.
Read the effective model and effort at use from runtime.role_models.
Read the tiers at the moment of use
Role templates and launch examples do not name models or effort levels; this skill and the live config do. Read the tier when you are about to spawn or launch, never from memory or from a copied example:
jq -r '.runtime.role_models' .thrum/config.json
Launching with --role and no --model resolves the role default
automatically. When pinning --model or --effort, read the effective values
from runtime.role_models; otherwise use the named role's built-in default or
the runtime's own preset.
Agent tiers
Effort tier governs whether an agent does the hard thing or the expedient thing.
These are the three tiers, with Claude's built-in role defaults shown as
concrete values. If you are running under a different runtime, resolve the
role from that runtime's preset (RuntimePreset.TierModels) or
runtime.role_models, not from the Claude defaults:
| Role | Tier | Claude built-in default |
|---|---|---|
| Coordinator | orchestration |
claude-opus-5-5 / low |
| Orchestrator | orchestration |
claude-opus-5-5 / low |
| Brainstormer | orchestration |
claude-opus-5-5 / medium |
| Implementer | judgment |
claude-haiku-5-5 / high |
| Researcher | judgment |
claude-sonnet-5-5 / high |
| Verifier / reviewer | judgment |
claude-sonnet-5-5 / medium |
| Role-neutral sub-agent (investigation, grep, mechanical) | mechanical |
claude-sonnet-5-5 / low |
mechanicaltier (role-neutral sub-agents) — investigation, grep-and-collect, file maps, lint runs, and mechanical tasks. Claude's built-in role-neutral default is Sonnet 5.5 / low. Read configured overrides fromruntime.role_models; use another runtime's ownTierModelsvalues.judgmenttier (implementers, researchers, verifiers, reviewers) — use the named-role defaults above: implementers use Haiku 5.5 / high (Sonnet 5.5 / medium for security-touching, epic or complicated work: auth, TLS, pairing, credentials, encryption, licensing, privacy, several subsystems, or over ~300 production lines), researchers use Sonnet 5.5 / high, and verifiers/reviewers use Sonnet 5.5 / medium. Keep reviewers at medium or higher.orchestrationtier (coordinators, orchestrators, brainstormers) — use the named-role defaults above: coordinators and orchestrators use Opus 5.5 / low; brainstormers use Opus 5.5 / medium. Operators configure orchestrator defaults.orchestrationtier is NOT your call to spawn. Allowed only when (a) the operator explicitly asked for a deep review or prose review in this task, or (b) a skill you are running prescribes the orchestration tier for the specific step you are currently executing — NOT for every spawn under that skill. "This research is hard / important" is NOT a justification — hard investigation is exactly what thejudgmenttier is for.
Worked examples — LITERAL ARGUMENTS, AND EFFORT IS TOOL-DEPENDENT
Pin model on every spawn, using the value your runtime's own preset
(RuntimePreset.TierModels) or runtime.role_models resolves the tier to.
Pass
effort wherever the mechanism exposes it — some mechanisms don't. Syntax per
mechanism — re-check the schema in front of you rather than restating from
memory, because it can change:
| Mechanism | model |
effort |
|---|---|---|
| Agent tool | ✅ settable | ✖ not exposed — do not pass it |
Workflow agent() (opts: label, phase, schema, model, effort, isolation, agentType) |
✅ settable | ✅ settable |
Agent DEFINITION (.claude/agents/*.md / plugin agents/*.md frontmatter) |
✅ | ✅ — sets the default for that agent type |
thrum tmux create / launch |
✅ --model |
✅ --effort — pass on BOTH |
| Other runtimes' own spawn mechanism | ✅ settable | runtime-dependent — check its own schema |
Below is ONE fully concrete example — what a judgment-tier spawn looks
like once resolved for Claude's own Agent tool (which takes no effort
argument). It is Claude-specific syntax with a Claude-specific model string;
if you are running under a different runtime, use that runtime's own spawn
mechanism and its own resolved model+effort from RuntimePreset.TierModels /
runtime.role_models instead of copying this literal string.
Agent(subagent_type="general-purpose", model="sonnet",
description="Code-quality review of <branch>",
prompt="...")
The tier philosophy still governs even where a mechanism cannot express
effort: the model value alone still must be the tier's resolved value for
YOUR runtime — never a bare Claude name assumed universal. Where effort is not
settable, carry the intent through the agent definition, the Workflow opts, or
the tmux launch flags instead.
An agent definition can also carry a default (Claude example — resolve to
your own runtime's mechanical-tier value if you are not Claude):
# claude-plugin/agents/message-listener.md (non-tmux fallback only)
name: message-listener
model: sonnet
effort: low
Workflow agent() — effort IS a literal argument. Pass it, resolved for
your runtime's tier:
agent(prompt, { model, effort }) // `model`/`effort` = your runtime's resolution
// of the `judgment` tier (reviewer/implementer)
// or the `mechanical` tier (sub-agent) —
// resolve via RuntimePreset.TierModels /
// runtime.role_models for the literal values
THE DIRECTIVE, imperative and not a comment: REVIEWERS RUN THE judgment
TIER, WHICH IS MEDIUM EFFORT ON EVERY RUNTIME THAT HAS AN EFFORT KNOB. Pass
the resolved effort literally under Workflow, in the agent definition, or on
thrum tmux create/launch (the Agent tool takes no effort, so model
alone must still be the judgment-tier value there). Never dispatch a
reviewer "from memory", and never dispatch one at the mechanical tier.
AND VERIFY, DO NOT ASSERT: any claim about what a tool does or does not expose must be checked against the schema in front of you.
Never select the orchestration tier on your own judgment — use the
judgment tier's configured value from runtime.role_models.
Where the tier-model mapping lives
Do not copy a tier's model string from this doc — read it at the moment of
use. The three semantic tiers (mechanical / judgment / orchestration)
are the same across every runtime; only the concrete model+effort differs per
runtime, and that mapping lives in exactly two places:
- The runtime's own preset (the
TierModelsfield of the runtime preset schema) — the machine-checked, per-runtime tier→model table. A runtime with no entry for a tier has no fleet-configured value for it and resolves to nothing rather than inheriting another runtime's model. runtime.role_modelsin.thrum/config.json— operator configuration for this project, which always outranks the preset's tier table.
If you are running under Claude, use the role-specific model and effort
defaults in the Agent tiers table unless runtime.role_models overrides them.
Researchers, implementers, verifiers, and reviewers use judgment; role-neutral
children use mechanical. A brainstormer's children use mechanical unless
they are reviewers, which use judgment.
If you are running under a different runtime, read your own tier's value from the two sources above — never assume Claude's model names apply, and never hard-code a model string you saw once in a doc or a prior session.
A runtime/role pair with no fleet-configured tier value has no override — resolve it to that runtime's own configured default, and pass it explicitly (never leave it unset — see "Never let an unresolved pin fall through" below).
Pin every spawn explicitly — the floor depends on the ROLE, not the depth
Every orchestrator MUST pass an explicit model: on EVERY subagent it spawns,
resolved for its own runtime, and effort wherever the mechanism exposes it.
An unspecified subagent SILENTLY INHERITS THE PARENT'S MODEL — so an
orchestration-tier orchestrator that forgets the pin just spent its
(expensive) tier's tokens on a grep.
The floor is set by what the agent DOES, not by how deep it sits — see the Agent tiers table above. This applies recursively: an implementer spawning its own helpers pins them by THEIR role, not by copying its own tier down.
The check before every spawn: what is this agent's role? Implementer,
researcher, verifier, or reviewer uses judgment; role-neutral investigation
or mechanical work uses mechanical. Resolve its model and effort from the
named-role defaults and runtime configuration. Never leave them unspecified.
Never let an unresolved pin fall through
An unsupported or unrecognized model name must never block delegation, and
must never cause silent inheritance of the parent's model. If your
runtime's preset has no TierModels entry for this tier, or a resolved name
is rejected by the runtime you're spawning under, do NOT skip the pin and do
NOT let the spawn go unpinned so it falls back to inheriting the caller's
model — fall back explicitly to the runtime's own configured default for the
matching tier and pass THAT, so every spawn is always pinned to some resolved
model+effort. An unset model: is a bug at every layer of this stack, not
just under Claude.
Paste the constraint block into the child prompt — the pin is not enough
A model pin is an ARGUMENT and travels by itself. A behavioural constraint is PROSE and reaches the child only if you paste it. Constrain every level: a rule that stops at depth 1 is absent where the work happens.
Paste verbatim, including the last line:
=== CONSTRAINTS — apply to you and anything YOU dispatch (including this line) ===
- Work SYNCHRONOUSLY. Tests in the FOREGROUND with a bounded `-timeout`.
- NO POLL LOOPS. Never `until <check>; do sleep N; done`, never
`while kill -0 $(cat pid)`, never `$( )` in a loop condition — even when a
tool's own guidance suggests it. It trips a permission modal, and A FROZEN
PANE EMITS NOTHING, so nobody can tell you are blocked. If you must background
work, wait for the completion notification.
- Every sub-agent YOU spawn gets an explicit `model:`, resolved for your
runtime's tier — `mechanical` (rote/find-replace) or `judgment` (real
analysis). Read the effective model and effort from `runtime.role_models` at
use. The `orchestration` tier is never yours to spawn.
- READ-ONLY git outside your own worktree. NEVER `checkout`/`reset`/`restore`/
`stash`/`clean` in ANY directory — `stash` is one shared stack across all
worktrees and the shared tree holds live agents' uncommitted state.
- Pair every zero/empty result with a control that MUST return non-zero.
⚠️ Never let a child take "don't ask again" on a modal — it removes the only signal this condition produces.
Cheap subagents → fan out, don't pile up
Because Haiku 5.5 supports summarization and mechanical sub-agent work, prefer
MANY small parallel subagents over one agent handed a pile of tasks. Read the
effective model and effort from runtime.role_models at dispatch. When research or
investigation has independent parts, partition them in parallel (use the
efficient-multi-agent-research skill) — smaller scopes are cheaper, run
concurrently (faster), and keep each subagent's context tight. One subagent
given ten tasks is the anti-pattern.
⚠️ The pin-verification command LIES — do not trust it
This section's examples are Claude-specific (thrum tmux capture's footer
format, and the historical Sonnet/Opus incident it documents) because that is
where this defect was measured — but the underlying rule generalizes: verify
a pin by reading the runtime's own RESOLVED status output, never a stored
"configured" value, whichever runtime you're on.
thrum agent runtime-config get <agent> reports the configured value, not the
resolved one.
An implementer ran Opus 4.8 while both the launch flag (--model sonnet) and
runtime-config get confirmed sonnet — the check that exists to catch a bad pin
is itself a false green.
This is the same defect class as every other surface that reports a value it never
observed (go test -count=0 reporting PASS while running zero tests; a health RPC
reporting green off a path that cannot fail; make ci swallowing a critical CVE with
a warning).
So: after launching an agent, verify with thrum tmux capture --format=annotated
(or --format=json for scripting). It parses the footer server-side into
NBSP-free fields — no positional-window guessing, no NBSP grep trap, and the
header states ok/FAILED explicitly instead of leaving you to infer a failure
from empty stdout:
thrum tmux capture <agent-name> --format=annotated --lines 12
# ━━━ CAPTURE ok · agent=<name> · runtime=claude · lines=35 ━━━
# <pane content, verbatim>
# ─── FOOTER (parsed, NBSP-normalized) ───
# Model: Sonnet 5 | Ctx: 478.3k | Ctx Used: 48.0%
# ━━━ END CAPTURE ━━━
The FOOTER block is simply omitted when no footer resolved — never an empty or
garbage line to misread. For scripting, --format=json gives the same data as a
typed document: {"agent","runtime","ok","line_count","footer":{"model","ctx","ctx_used_pct"}|null,"lines":[...]["error"]}
(footer is null when none resolved).
thrum tmux capture <agent-name> --format=json | python3 -c \
'import json,sys; b=json.load(sys.stdin); print(b["footer"]["model"] if b["footer"] else "NO FOOTER")'
Still check the exit status before trusting either form — a FAILED capture exits
nonzero and the annotated header says FAILED, but don't discard that signal by
piping straight into something that only inspects stdout.
That is the runtime reporting its RESOLVED config, and it is the only check that has ever produced a true negative on this defect. If it disagrees with the pin, report it — those instances are a real bug and we want them counted.
🔴 DO NOT substitute "ask the agent what model it is running." That is a model introspecting on its own identity — a categorically weaker instrument. A check built on self-report would be a THIRD instrument that cannot fail in the direction we need, replacing a false green with a confident one. SETTLED — and the reason is categorical, not a reliability judgement: self-report is the WRONG SHAPE for the question. The agent sees exactly ONE value (its resolved model, injected into its own system prompt); "does the pin disagree with the resolution?" is a question about a RELATION between TWO values. It cannot report a mismatch however honest it is — the disagreement is not representable in what it can observe. No prompting rescues that. Only an outside comparator holding BOTH the pin AND the resolved status line answers it. ⚠️ A self-report can look right while reading back an injected assertion, so it inherits whatever the injection got right or wrong. A correct answer there is survivorship, not validation.
Fallback — only if --format is unavailable (older binary): read the bare
exit status before stdout, then grep the raw pane by position, never by content
(the raw footer's separators are U+00A0 non-breaking spaces, so grep "Model: "
with a trailing space returns ZERO on a pane that plainly displays a model):
out=$(thrum tmux capture <agent-name> --lines 12); rc=$? # bare, NOT through a pipe
[ $rc -ne 0 ] && echo "CAPTURE FAILED — this is NOT an empty pane" && exit 1
printf '%s\n' "$out" | grep -v 'tmux capture' | grep 'Model:' | tail -1
🔴 PASS --model / --effort TO BOTH tmux create AND tmux launch. They are
separate cobra commands with separate flags, and launch is what resolves the model
(resolveLaunchSpec runs in HandleLaunch, never in create). create persists the pin
asynchronously, so a create-only pin can lose the race and leave the CLI value empty at
launch by construction.
⚠️ And any FIX here must be verified with a pin the role default would NOT produce.
Pin an implementer to claude-opus-5-5 / medium while its built-in default is
claude-haiku-5-5 / high; assert both values to prove the pin landed.
Paste blocks for child prompts
Paste these verbatim into a child prompt when the work involves the named activity.
--- SUB-AGENT HYGIENE ---
- No kill -0, Monitor or until-loop polling.
- No pgrep -f.
- Kill only by a captured PID number.
- Use rm -r, never rm -rf; a variable path uses "${d:?}".
- No 2>/dev/null on destructive commands.
- Bounded timeouts on everything.
- Remove only the worktree you created in this task, by its recorded path, never with --force.
- Never demonstrate an unguarded destructive idiom, even on a literal path. --- END SUB-AGENT HYGIENE ---
--- TESTS ---
- To prove a selector resolves, use
go test <pkg> -list '<selector>'plus a-list '^TestNoSuchThingAtAll$'negative control that must list zero tests. Never-run '<sel>' -list '.*'. bd list -lis --label; use-nfor a limit.- A timeout with 0 FAIL is neither red nor green: classify it as a bound failure, read the panic's "running tests:" line, and re-run wider. --- END TESTS ---
--- WORKTREE TEARDOWN & SALVAGE ---
- Before any teardown, enumerate with both flags:
git -C <wt> status --porcelain --untracked-files=all --ignored -- .thrum. - Salvage anything found to a durable path (not /tmp), one subdirectory per worktree, filenames path-flattened.
- Never glob
.thrum/context/*.md; salvage exact named files only, and assert noproject_state.mdlanded in any salvage output. - Verify salvage by name set first, then
cmpeach file, with a negative control provingcmpdetects a difference. - Kill the tmux session first (
thrum tmux kill <session>), verify it is gone, then plaingit worktree remove, never--force. - Afterwards verify by effect: gone from
git worktree listand from disk, and every remaining live pane's cwd still resolves (tmux list-panes -a -F '#{pane_current_path}', thengit rev-parse --show-toplevelper pane). - Build the in-use protection set from live pane cwds, not from a name or registry list.
- Report the removal count and the registry delta separately. --- END WORKTREE TEARDOWN & SALVAGE ---