SLOPSHOPPER

routing-mod

Routes each subagent to Haiku, Sonnet or Opus by the [tier:...] tag from your plan. The main session never switches model, so its prompt cache stays intact.

newmodelagents
v0.1.0no licenseupdated 2026-10-07blackmath88/routing-mod-claude
A shopper browsing a rack in a slop shop
README

routing-mod-claude

Claude Code mod: plan in chat, route execution by tier.

Each subagent's model comes from the [tier:...] tag in its task: light → Haiku · standard → Sonnet · deep → Opus. Untagged tasks are classified by Haiku. Above 85 % rate-limit usage, tasks step down one tier. Every subagent is told to return a summary under 150 words.

The main session never switches model, so its prompt cache stays intact.

Requirements

Claude Code v2.1.287+ (mods).

Try it

claude --plugin-dir ./routing-mod-claude
claude plugin validate ./routing-mod-claude

Install from GitHub

/plugin marketplace add blackmath88/routing-mod-claude
/plugin install routing-mod@routing-mod-claude

New machine

Clone this repository, then run:

./setup.sh
./setup.sh --check

Recommended setup

  • Main model: /model sonnet
  • Advisor: /advisor opus (subagents inherit it when the pairing is valid)

Per project

  1. Copy templates/CLAUDE-snippet.md into the project's CLAUDE.md.
  2. Put plans from chat into plans/<feature>.md (format: templates/PLAN.md).
  3. In Claude Code: "Execute plans/<feature>.md".

Tune tiers and the limit at the top of hooks/register.js.

Source 1 files
hooks/register.js 49 lines
1// routing-mod: route subagents by tier. Main session is never switched (keeps its cache).
2
3const TIERS = { light: 'haiku', standard: 'sonnet', deep: 'opus' }
4const STEP_DOWN = { deep: 'standard', standard: 'light', light: 'light' }
5const LIMIT_PERCENT = 85 // step down one tier above this rate-limit usage
6const TAG = /\[tier:(light|standard|deep)\]/i
7const RETURN_RULE =
8  '\n\nWhen done, reply with a summary under 150 words: what you changed (files), ' +
9  'what you verified, open issues. No full file contents, no long logs.'
10
11export function register(on) {
12  on('agent.spawn', async ($, e, next) => {
13    // Forks always inherit the parent's model; leave them alone
14    if (e.fork) return next(e)
15
16    let tier = e.prompt.match(TAG)?.[1]?.toLowerCase()
17    let source = 'Plan'
18
19    // No tag but the caller named a model explicitly: respect it
20    if (!tier && e.model) return next(e)
21
22    if (!tier) {
23      try {
24        tier = await $.model.classify(
25          `${e.description}\n\n${e.prompt.slice(0, 2000)}`,
26          ['light', 'standard', 'deep'],
27          { model: 'haiku' }
28        )
29        source = 'klassifiziert'
30      } catch {}
31      if (!tier) { tier = 'standard'; source = 'Standard' }
32    }
33
34    try {
35      const { rateLimits = [] } = await $.session.usage()
36      if (rateLimits.some(r => r.percentUsed >= LIMIT_PERCENT) && tier !== STEP_DOWN[tier]) {
37        tier = STEP_DOWN[tier]
38        source += ', Limit-Schutz'
39      }
40    } catch {}
41
42    const model = TIERS[tier]
43    const prompt = e.prompt.replace(TAG, '').trim() + RETURN_RULE
44    const result = await next({ ...e, model, prompt })
45    if (!result.deny) $.ui.log(`${e.description} → ${tier} (${model}) · ${source}`)
46    return result
47  })
48}
49