resolve-archetype-model — archetype→model resolution for native sub-agent dispatch
1. What this is, and is not
This is the native-dispatch counterpart to hera worker spawn's archetype resolution. A hera worker
gets its model from the project's bound diligence profile automatically (hera_spawn_worker's
archetype param drives internal/agent.ResolveModel at spawn time). A native sub-agent — spawned
via the Agent/Task tool, or via a Workflow script's agent() — has no such path: it silently
runs at whatever model the calling session inherits, regardless of what the project's profile says
that kind of work should run at.
This skill is the convention that closes that gap. It is not a new MCP tool — profile_resolve
already returns everything needed in one call. It is not specific to review panels —
hera-spawn-review already does exactly this pattern for the review archetype's [panel] block;
this skill generalizes it to any archetype a pipeline's stages map to.
2. Resolve once per pipeline
Call mcp__argus__profile_resolve(cwd=$PWD) exactly once per pipeline or session, not once per
stage — the response already carries every archetype's entry. Build a local map from it:
archetypes = resolved.archetype # {} if resolved.resolved == false
The response shape (per internal/mcp/profiles.go):
{"resolved": true|false, "name": "...", "source": "...",
"archetype": {"code_slice": {"model": "sonnet", "effort": ""}, "review": {"model": "opus", "effort": ""}, ...},
"rigor": {...}, "panel": {...}, "errors": [...]}
Field names are lowercase/snake_case (model, effort, window) — read them by exact key, no
case-normalization needed.
3. Fail-open fallback
Never treat a miss as an error — always fall back to the dispatch mechanism's own default model:
resolved: false(no profile, invalid profile, malformed[panel]) — every stage in the pipeline dispatches with no model override.- A specific archetype absent from
archetype, or present with an emptymodel— only that stage falls back; other stages whose archetypes ARE present still get their resolved model. A profile author may legitimately leave some archetypes unset.
4. The in-session model gate (mandatory before dispatch)
A profile's archetype model is validated against the union of every configured backend's
models — it may legitimately name a codex model, not just a Claude one. Claude's native sub-agent
dispatch only runs in-session Claude models. Before threading a resolved model into a dispatch
call, check it against the same four values hera-spawn-review already checks finders against
(mirrors internal/review.knownInSessionModels):
knownInSession = {"opus", "sonnet", "haiku", "fable"}
- Model is one of these four → forward it:
Agent(model=<resolved>)or, for aWorkflowscript,agent(prompt, {model: <resolved>}). - Model is anything else (a foreign backend's model name, e.g. a codex model) → native
dispatch has no path to a different backend at all — it can only ever spawn one of the four
in-session Claude models. Rather than dropping model selection entirely, map to the closest
available in-session Claude model and forward that instead:
- A foreign flagship/top-tier model (e.g.
gpt-5,gpt-5-codex, or any other backend's highest-capability model) →opus. - A foreign backend's smaller/cheaper tier (e.g. a
-mini-class model) →haiku. - Anything ambiguous, or a foreign model whose tier isn't obvious from its name →
sonnet(the safe middle default). - Always emit a loud, visible note when this substitution happens — e.g.
[resolve-archetype-model] archetype "code_slice" resolved to a non-in-session model ("gpt-5-codex") — substituting "opus" (closest in-session equivalent) for native dispatch.Never silently substitute; the caller (or a report reader) needs to know the profile's actual choice wasn't honored. This tiering is a best-effort heuristic, not a principled cross-vendor equivalence — there is no validated quality mapping between vendors' model tiers; it exists so native dispatch degrades gracefully (some in-session model, correctly tiered by rough capability) instead of silently reverting to the caller's own default, which could be any tier regardless of what the archetype was configured for.
- A foreign flagship/top-tier model (e.g.
5. Effort — only where the mechanism accepts it
An archetype's effort field (low/medium/high) is a real, validated part of the profile, but
whether it can be applied depends entirely on the dispatch mechanism:
- Claude's built-in
Agent/Tasktool has no effort parameter as of this writing. Check the tool's current schema before assuming otherwise — if it gains one later, threadeffort=the same way asmodel=, gated the same way. Until then, effort is unusable here: omit it, and don't imply in a report that it was applied. Workflow'sagent()acceptsopts.effort('low'|'medium'|'high'|'xhigh'|'max'— a strict superset of a profile's three-value enum). When dispatching through aWorkflowscript, thread the resolvedeffortstraight intoopts.effort— no gate needed beyond "non-empty."
This mirrors the already-documented Fable-effort gotcha in hera-spawn-review (§12): a real
capability gap in the current tooling, not a design choice — don't let a report claim effort was
honored by a mechanism that has no way to honor it.
6. Worked example
resolved = profile_resolve(cwd=$PWD)
models = resolved.archetype if resolved.resolved else {}
knownInSession = {"opus", "sonnet", "haiku", "fable"}
foreignFlagshipHints = ["gpt-5", "opus", "large", "pro"] # rough, name-based, best-effort
foreignCheapHints = ["mini", "haiku", "small", "flash"]
def modelFor(archetype):
entry = models.get(archetype, {})
m = entry.get("model", "")
if not m:
return None # unset — use the caller's/tool's own default
if m in knownInSession:
return m # forward as-is
# Foreign backend model — native dispatch can't spawn it at all. Substitute the
# closest in-session tier rather than dropping model selection entirely.
substitute = "sonnet"
if any(h in m for h in foreignFlagshipHints):
substitute = "opus"
elif any(h in m for h in foreignCheapHints):
substitute = "haiku"
note(f'[resolve-archetype-model] archetype "{archetype}" resolved to a non-in-session '
f'model ("{m}") — substituting "{substitute}" (closest in-session equivalent) for '
f'native dispatch.')
return substitute
# Agent tool (no effort parameter available):
Agent(prompt=migration_prompt, model=modelFor("code_slice")) # e.g. "sonnet", or omitted
Agent(prompt=review_prompt, model=modelFor("review")) # e.g. "opus", or omitted
# Workflow script's agent() (effort IS available):
entry = models.get("ci_loop", {})
await agent(ci_fix_prompt, {
model: modelFor("ci_loop"),
effort: entry.get("effort") or undefined, # omit rather than pass an empty string
})
7. Gotchas
- One
profile_resolvecall per pipeline, not per stage. The whole point of returning every archetype's entry in one response is to avoid N round-trips for an N-stage pipeline. - The in-session gate is not optional, and a mismatch is a substitution, not a silent drop. Native dispatch cannot spawn a different backend at all — an archetype tuned for a codex worker must fall back to the closest in-session Claude tier (§4), loudly noted, rather than either erroring or quietly running with no model override (which could land on any tier, unrelated to what the archetype was configured for).
- Don't claim effort was applied when it wasn't. The
Agenttool's lack of an effort parameter is a real, current limitation — state it plainly in any report, the same wayhera-spawn-reviewdocuments its Fable-effort gap rather than silently ignoring it. - This skill does not modify
hera,hera-plan,hera-review, orhera-spawn-review. It is a standalone reference;hera-spawn-reviewis prior art for this same pattern (already resolvesprofile_resolve'spanelblock and gates finders the same way), not something this skill wraps or depends on.