SLOPSHOPPER

delegation

Pick model and effort for subagents, workflow agents and new sessions, plus a saved delegation note

newpanebandguardcommandprompt
v0.4.0MITupdated 2026-10-24anas1412/claude-delegation
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · delegation
│ ┃ Delegation ✕ › fix the failing auth test and add an audit log call │ ┃ Choose how Claude delegates. Auto leaves the │ ┃ choice to Claude. ⏺ Read(src/auth.ts) │ ┃ ╭──────────────────────────────────────────╮ ⎿ Read 6 lines │ ┃ │Agents Auto│ ⏺ Update(src/auth.ts) │ ┃ │Subagents Claude starts with the Agent │ ⎿ Added 2 lines, removed 1 line │ ┃ │tool. Enforced. │ ⏺ Bash(bun test) │ ┃ │Model: Auto ▾Effort: Auto ▾ │ ⎿ 3 pass, 1 fail │ ┃ ╰──────────────────────────────────────────╯ │ ┃ ╭──────────────────────────────────────────╮ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │Workflow agents Auto│ │ ┃ │Each agent() in a workflow script. Sent as│ ✻ Worked for 42s · done 4:20 PM │ ┃ │instructions: Workflow has no model field │ │ ┃ │to enforce. │ › /delegation │ ┃ │Model: Auto ▾Effort: Auto ▾ │ ⎿ delegation: Delegation pane opened. │ ┃ ╰──────────────────────────────────────────╯ │ ┃ ╭──────────────────────────────────────────╮ │ ┃ │Nested sessions Auto│ │ ┃ │New sessions Claude starts. Enforced on │ │ ┃ │fresh sessions, capped at this session’s │ │ ┃ │model and effort; forks inherit. │ │ ┃ │Model: Auto ▾Effort: Auto ▾ │ │ ┃ ╰──────────────────────────────────────────╯ │ ┃ ╭─────────────────────────────────────────── │ ┃ │Delegation note Empty │ ┃ │Sent with every request. Say which model ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Delegation
Choose how Claude delegates. Auto leaves the choice to Claude. ╭──────────────────────────────────────────────────────────╮ │Agents Auto│ │Subagents Claude starts with the Agent tool. Enforced. │ │Model: Auto ▾Effort: Auto ▾ │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │Workflow agents Auto│ │Each agent() in a workflow script. Sent as instructions: │ │Workflow has no model field to enforce. │ │Model: Auto ▾Effort: Auto ▾ │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │Nested sessions Auto│ │New sessions Claude starts. Enforced on fresh sessions, │ │capped at this session’s model and effort; forks inherit. │ │Model: Auto ▾Effort: Auto ▾ │ ╰──────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────── │Delegation note Empty │Sent with every request. Say which model each kind of task │should get. │planning/review: opus high coding: sonnet medium search/readi ╰───────────────────────────────────────────────────────────
README

delegation

A Claude Code mod that lets you pick the model and effort Claude uses when it delegates work, and keeps a delegation note that goes with every request.

What it does

Run /delegation to open the side pane:

  • Agents: model and effort for Agent tool subagents.
  • Workflow agents: model and effort for the agents a Workflow script spawns.
  • Nested sessions: model and effort for sessions Claude starts.
  • Note: free text, saved as you type and kept across sessions. Example:
  planning/review: opus high
  coding: sonnet medium
  search/reading: haiku low

Every picker defaults to auto, which leaves the choice to Claude. Reset all puts everything back to auto.

While agents, workflow agents or nested sessions are running, a line above the prompt shows what they run with (◆ Delegation Agents 2 · haiku Nested sessions 1 · sonnet · low). It stays hidden when nothing is running or every setting is on auto.

How it applies

  • Your picks and note are added to Claude's system prompt on every request, so Claude follows them when planning agents, workflows and sessions.
  • Agent calls and fresh new sessions are forced to the model and effort you picked, whatever Claude chose. A session can't be started above this session's own model or effort, so higher picks are capped there; forked sessions inherit this one's.
  • Workflow agents are not forced: the Workflow tool has no model field, so the mod tells Claude to put your model and effort in every agent() call of the script.

Install

At the prompt of a terminal session:

/plugin install delegation --marketplace anas1412/claude-delegation

From a shell:

claude plugin install delegation --marketplace anas1412/claude-delegation

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with anas1412/claude-delegation, then Add plugin.

Test

claude plugin test .

License

MIT

Source 2 files
hooks/register.tsx 306 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Prefs, Running } from '../types'
5
6const PANE = 'delegation'
7const AUTO = 'auto'
8const MODELS = [AUTO, 'fable', 'opus', 'sonnet', 'haiku']
9const EFFORTS = [AUTO, 'low', 'medium', 'high', 'xhigh', 'max']
10// start_session takes full ids, not aliases.
11const MODEL_IDS: Record<string, string> = {
12  fable: 'claude-fable-5-1',
13  opus: 'claude-opus-5-5',
14  sonnet: 'claude-sonnet-5-5',
15  haiku: 'claude-haiku-5-5',
16}
17const DEFAULTS: Prefs = {
18  agentModel: AUTO,
19  agentEffort: AUTO,
20  workflowModel: AUTO,
21  workflowEffort: AUTO,
22  sessionModel: AUTO,
23  sessionEffort: AUTO,
24  note: '',
25}
26
27type SessionRow = { startedBy?: string; isRunning?: boolean }
28
29type Group = {
30  kind: keyof Running
31  title: string
32  hint: string
33  model: keyof Prefs
34  effort: keyof Prefs
35}
36const GROUPS: Group[] = [
37  {
38    kind: 'agents',
39    title: 'Agents',
40    hint: 'Subagents Claude starts with the Agent tool. Enforced.',
41    model: 'agentModel',
42    effort: 'agentEffort',
43  },
44  {
45    kind: 'workflow',
46    title: 'Workflow agents',
47    hint: 'Each agent() in a workflow script. Sent as instructions: Workflow has no model field to enforce.',
48    model: 'workflowModel',
49    effort: 'workflowEffort',
50  },
51  {
52    kind: 'sessions',
53    title: 'Nested sessions',
54    hint: 'New sessions Claude starts. Enforced on fresh sessions, capped at this session’s model and effort; forks inherit.',
55    model: 'sessionModel',
56    effort: 'sessionEffort',
57  },
58]
59
60const prefs = atom({ plugin: 'delegation', key: 'prefs' } as const, DEFAULTS)
61const running = atom({ plugin: 'delegation', key: 'running' } as const, {
62  agents: 0,
63  workflow: 0,
64  sessions: 0,
65} as Running)
66
67const set = (b: EngineInterface, patch: Partial<Prefs>) =>
68  update(b, prefs, p => {
69    const next = { ...p, ...patch }
70    void b.store.set('prefs', next)
71    return next
72  })
73
74const pick = (v: string) => (v === AUTO ? undefined : v)
75
76const label = (p: Prefs, g: Group) =>
77  [p[g.model], p[g.effort]].filter(v => v !== AUTO).join(' · ') || AUTO
78
79const section = (p: Prefs) => {
80  const note = p.note.trim()
81  return [
82    'The user picked these models and effort levels for delegated work ("auto" means choose yourself). They override any default rule about which model agents use.',
83    `- Agent tool subagents: model ${p.agentModel}, effort ${p.agentEffort}`,
84    `- Workflow agents: model ${p.workflowModel}, effort ${p.workflowEffort}. When writing a Workflow script, put these in every agent() call's opts unless "auto".`,
85    `- New sessions (start_session): model ${p.sessionModel}, effort ${p.sessionEffort}`,
86    note &&
87      `The user's delegation note: follow it when splitting work into agents, workflow tasks or sessions (e.g. which model each kind of task gets):\n${note}`,
88  ]
89    .filter(Boolean)
90    .join('\n')
91}
92
93// Running counts. Subagent and workflow-agent loops show up as turns with an
94// agentId; only Agent-tool agents are in $.agent.list(). Nested sessions run
95// in their own process, so they are polled while any is running.
96const loops = new Map<string, 'agents' | 'workflow'>()
97let hasWorkflow = false
98let poll: { cancel: () => void } | undefined
99
100const recount = ($: EngineInterface) =>
101  update($, running, r => ({
102    ...r,
103    agents: [...loops.values()].filter(k => k === 'agents').length,
104    workflow: [...loops.values()].filter(k => k === 'workflow').length,
105  }))
106
107const pollSessions = async ($: EngineInterface) => {
108  const res = await $.mcp.call('ccd_session_mgmt', 'list_sessions', { linked: true })
109  const block = res.content[0]
110  const rows = block?.type === 'text' ? (JSON.parse(block.text ?? '[]') as SessionRow[]) : []
111  const sessions = rows.filter(r => r.startedBy && r.isRunning).length
112  await update($, running, r => ({ ...r, sessions }))
113  if (sessions === 0) {
114    poll?.cancel()
115    poll = undefined
116  }
117}
118
119export const register: Register = on => {
120  on('session.start', async ($, e, next) => {
121    const saved = (await $.store.get('prefs')) as Partial<Prefs> | undefined
122    if (saved) await update($, prefs, () => ({ ...DEFAULTS, ...saved }))
123    await $.command.register({
124      name: 'delegation',
125      description: 'Pick model/effort for agents, workflows and sessions',
126    })
127    return next(e)
128  })
129
130  on('command.run', { command: 'delegation' }, async $ => {
131    await $.ui.open({ id: PANE, title: 'Delegation' })
132    return { text: 'Delegation pane opened.' }
133  })
134
135  on('prompt.compose', async ($, e, next) => {
136    const out = await next(e)
137    const text = section(await read($, prefs))
138    return {
139      sections: [...out.sections, { id: 'delegation:prefs', text, scope: 'session' }],
140    }
141  })
142
143  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
144    const p = await read($, prefs)
145    const model = pick(p.agentModel) as typeof e.model
146    const effort = pick(p.agentEffort) as typeof e.effort
147    return next({ ...e, ...(model && { model }), ...(effort && { effort }) })
148  }).catch(($, e, next) => next(e))
149
150  on('tool.call', { tool: 'mcp__ccd_session__start_session' }, async ($, e, next) => {
151    const input = e as typeof e & { context?: string; proposal_id?: string }
152    // model/effort apply to fresh sessions only; a proposal starts exactly as proposed.
153    if (input.context === 'fork' || input.proposal_id) return next(e)
154    const p = await read($, prefs)
155    const model = pick(p.sessionModel)
156    const effort = pick(p.sessionEffort)
157    return next({
158      ...e,
159      ...(model && { model: MODEL_IDS[model] }),
160      ...(effort && { effort }),
161    })
162  }).catch(($, e, next) => next(e))
163
164  on('tool.call', { tool: 'Workflow' }, ($, e, next) => {
165    hasWorkflow = true
166    return next(e)
167  })
168
169  on('tool.call', { tool: 'mcp__ccd_session__start_session' }, async ($, e, next) => {
170    const ran = await next(e)
171    // ponytail: 20s poll of list_sessions; switch to an event if the app ever raises one.
172    poll ??= $.clock.every(20_000, () => void pollSessions($).catch(() => {}))
173    void pollSessions($).catch(() => {})
174    return ran
175  })
176
177  on('turn.start', async ($, e, next) => {
178    const id = e.agentId
179    if (id !== undefined) {
180      const isAgent = (await $.agent.list()).some(a => a.id === id)
181      // Unlisted loops are workflow agents or the engine's own forks (compaction).
182      if (isAgent || hasWorkflow) {
183        loops.set(id, isAgent ? 'agents' : 'workflow')
184        await recount($)
185      }
186    }
187    return next(e)
188  })
189
190  on('turn.complete', async ($, e, next) => {
191    if (e.agentId !== undefined && loops.delete(e.agentId)) await recount($)
192    return next(e)
193  })
194
195  // Above the prompt, only while delegated work runs and the user set something.
196  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
197    const p = await read($, prefs)
198    const r = await read($, running)
199    const active = GROUPS.filter(g => r[g.kind] > 0)
200    if (e.props.hasSurvey || active.length === 0 || JSON.stringify(p) === JSON.stringify(DEFAULTS)) {
201      return next(e)
202    }
203    const { Text } = $.ui.resolve(e)
204
205    return (
206      <Text>
207        <Text color="claude">◆</Text> Delegation{'  '}
208        <Text dimColor>
209          {active.map(g => `${g.title} ${r[g.kind]} · ${label(p, g)}`).join('   ')}
210          {p.note.trim() ? '   · note on' : ''}
211        </Text>
212      </Text>
213    )
214  })
215
216  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
217    if (e.surface === 'mobile') {
218      const { Text } = $.ui.resolve(e)
219      return <Text dimColor>Open /delegation on desktop or terminal to edit.</Text>
220    }
221    const { Box, Text, Select, Input, Button } = $.ui.resolve(e)
222    const p = await read($, prefs)
223    const select = (key: keyof Prefs, title: string, list: string[]) => (
224      <Select
225        key={key}
226        label={title}
227        value={p[key]}
228        options={list.map(value => ({ value, label: value === AUTO ? 'Auto' : value }))}
229        onSelect={value => set($, { [key]: value })}
230      />
231    )
232    const hasNote = p.note.trim() !== ''
233    const isDefault = JSON.stringify(p) === JSON.stringify(DEFAULTS)
234
235    return (
236      <Box flexDirection="column" gap="md">
237        <Text dimColor size="footnote">
238          Choose how Claude delegates. Auto leaves the choice to Claude.
239        </Text>
240
241        {GROUPS.map(g => {
242          const value = label(p, g)
243          const isAuto = value === AUTO
244          return (
245            <Box
246              key={g.title}
247              flexDirection="column"
248              gap="sm"
249              padding="sm"
250              borderStyle="round"
251              borderColor={isAuto ? 'subtle' : 'claude'}
252            >
253              <Box flexDirection="row" justifyContent="space-between">
254                <Text weight="semibold">{g.title}</Text>
255                <Text size="caption" color={isAuto ? 'subtle' : 'claude'}>
256                  {isAuto ? 'Auto' : value}
257                </Text>
258              </Box>
259              <Text dimColor size="caption">
260                {g.hint}
261              </Text>
262              <Box flexDirection="row" flexWrap="wrap" gap="md">
263                {select(g.model, 'Model', MODELS)}
264                {select(g.effort, 'Effort', EFFORTS)}
265              </Box>
266            </Box>
267          )
268        })}
269
270        <Box
271          flexDirection="column"
272          gap="sm"
273          padding="sm"
274          borderStyle="round"
275          borderColor={hasNote ? 'claude' : 'subtle'}
276        >
277          <Box flexDirection="row" justifyContent="space-between">
278            <Text weight="semibold">Delegation note</Text>
279            <Text size="caption" color={hasNote ? 'success' : 'subtle'}>
280              {hasNote ? 'Saved' : 'Empty'}
281            </Text>
282          </Box>
283          <Text dimColor size="caption">
284            Sent with every request. Say which model each kind of task should get.
285          </Text>
286          <Input
287            key="note"
288            rows={5}
289            value={p.note}
290            placeholder={'planning/review: opus high\ncoding: sonnet medium\nsearch/reading: haiku low'}
291            submitLabel="save"
292            onInput={note => set($, { note })}
293            onSubmit={note => set($, { note })}
294          />
295        </Box>
296
297        {!isDefault && (
298          <Box flexDirection="row" justifyContent="flex-end">
299            <Button key="reset" label="Reset all" onPress={() => set($, DEFAULTS)} />
300          </Box>
301        )}
302      </Box>
303    )
304  })
305}
306
types/index.d.ts 18 lines
1export type Prefs = {
2  agentModel: string
3  agentEffort: string
4  workflowModel: string
5  workflowEffort: string
6  sessionModel: string
7  sessionEffort: string
8  note: string
9}
10
11export type Running = { agents: number; workflow: number; sessions: number }
12
13declare module 'claude-code' {
14  interface PluginState {
15    delegation: { prefs: Prefs; running: Running }
16  }
17}
18