Turns a session into an orchestrator that delegates work to haiku and sonnet subagents, keeps its own context small, sizes each task to about 150K tokens of…

This repository is a Claude Code plugin marketplace named rotem-mods. It holds three mods (plugins that add panes, bands, and commands to Claude Code).
| Mod | What it does |
|---|---|
context-usage | Shows how full the context window of the session is, in a band above the prompt. The /context-bar command toggles the detail view. |
session-topics | Shows the topics of the session and a color for each session above the prompt. You can tell side-by-side sessions apart. |
orchestrator | Makes the session delegate work to subagents. A subagent runs on Haiku for simple, repetitive tasks and on Sonnet for hard or long tasks, never on Opus or Fable. The orchestrator splits work into tasks of about 150K tokens of subagent context each. The details button in its band lists the running and past subagents. Use /orchestrator on, /orchestrator off, or /orchestrator status. |
/plugin marketplace add rotembm12/claude-mods
/plugin install context-usage@rotem-mods
/plugin install session-topics@rotem-mods
/plugin install orchestrator@rotem-mods
To get new versions of the mods, run this command:
/plugin marketplace update rotem-modshooks/register.tsx 383 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AgentRun, Saved } from '../types'
5import { CONTEXT_BUDGET, ORCHESTRATOR_PROMPT, REPORT_CONTRACT, ROLES } from './prompts'
6
7const IS_ON = { plugin: 'orchestrator', key: 'isOn' } as const
8const isOn = atom(IS_ON, false)
9const agents = atom({ plugin: 'orchestrator', key: 'agents' } as const, [])
10const isExpanded = atom({ plugin: 'orchestrator', key: 'isExpanded' } as const, false)
11const note = atom({ plugin: 'orchestrator', key: 'note' } as const, null)
12
13const SECTION_ID = 'orchestrator:mode'
14const ROLE_PREFIX = 'orchestrator:'
15const STORE_KEY = 'sessions'
16const KEEP_SESSIONS = 100
17const KEEP_AGENTS = 50
18// The tiers a model id can name, in the order the band lists them.
19const TIERS = ['fable', 'opus', 'sonnet', 'haiku']
20// The only tiers a subagent may run on.
21const ALLOWED_TIERS = ['sonnet', 'haiku']
22// The switch is two segments, off and on; the one that holds is filled.
23const OFF_KEY = 'orchestrator-off'
24const ON_KEY = 'orchestrator-on'
25const DETAILS_KEY = 'orchestrator-details'
26const OFF_FILL = '#4a4a4a'
27// The track under the segment that does not hold, so the two read as one control.
28const TRACK = '#262626'
29// The mark, the name, and the two segments take this many cells.
30const SWITCH_CELLS = 24
31// The rows the open list takes at most, its "more" row included.
32const MAX_LIST_ROWS = 12
33
34// About four characters a token: a direct result past this size gets a note.
35const BIG_RESULT_CHARS = 8000
36const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Bash', 'PowerShell', 'WebFetch', 'WebSearch'])
37
38const USAGE = 'Usage: /orchestrator [on|off|status]. With no argument it switches the mode.'
39const ON_NOTE =
40 'Orchestrator mode is now on for this session. From your next request, your system prompt carries the "Orchestrator mode" section. Follow it.'
41const OFF_NOTE =
42 'Orchestrator mode is now off. The "Orchestrator mode" section left your system prompt, so its delegation, model and report rules no longer apply. Work directly again.'
43const MODEL_DENY =
44 'Orchestrator mode: pass `model` (haiku or sonnet) on this Agent call. Pick it with the "Model and effort" rules in your system prompt, then call again.'
45const TIER_DENY =
46 'Orchestrator mode: subagents run on haiku or sonnet only, never opus or fable. Use haiku for repetitive, simple tasks and sonnet for hard or long ones, then call again.'
47const FORK_DENY =
48 'Orchestrator mode: forks are refused, because a fork runs on your own model. Spawn a haiku or sonnet agent with a brief that carries what it needs.'
49const SPAWN_DENY =
50 'Orchestrator mode: every subagent runs on haiku or sonnet. Give this agent `model: "haiku"` or `model: "sonnet"` (in a workflow script, in the options of its agent() call).'
51const DEPTH_DENY =
52 'Orchestrator mode: only the orchestrator spawns agents. Do this part yourself, or end with STATUS: blocked and say which extra agent you need.'
53const CHECK_DENY = 'Orchestrator mode could not check this spawn. Call it again.'
54
55function tierOf(model: string): string {
56 const id = model.toLowerCase()
57 return TIERS.find(t => id.includes(t)) ?? model
58}
59
60const isAllowed = (model: string) => ALLOWED_TIERS.includes(tierOf(model))
61
62function parse(args: string, current: boolean): boolean | 'status' | null {
63 const word = args.trim().toLowerCase()
64 if (word === '' || word === 'toggle') return !current
65 if (word === 'on') return true
66 if (word === 'off') return false
67 if (word === 'status') return 'status'
68 return null
69}
70
71function ledger(runs: AgentRun[]): string[] {
72 const counts: Record<string, number> = {}
73 for (const run of runs) counts[run.tier] = (counts[run.tier] ?? 0) + 1
74 const rank = (tier: string) => (TIERS.includes(tier) ? TIERS.indexOf(tier) : TIERS.length)
75 return Object.entries(counts)
76 .sort((a, b) => rank(a[0]) - rank(b[0]))
77 .map(([tier, n]) => `${n} ${tier}`)
78}
79
80function clip(text: string, width: number): string {
81 if (width <= 0) return ''
82 return text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}…`
83}
84
85function duration(ms: number): string {
86 const seconds = Math.max(0, Math.round(ms / 1000))
87 if (seconds < 60) return `${seconds}s`
88 const minutes = Math.floor(seconds / 60)
89 if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`
90 return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`
91}
92
93const GLYPHS: Record<AgentRun['status'], [string, string]> = {
94 running: ['●', 'warning'],
95 completed: ['✓', 'success'],
96 failed: ['✗', 'error'],
97 killed: ['■', 'inactive'],
98}
99
100// One line of the open list: who it is, its model, where it stands, how long, and what it did.
101function describe(run: AgentRun, now: number): string {
102 const label = run.name ?? run.description
103 const type = run.type.startsWith(ROLE_PREFIX) ? run.type.slice(ROLE_PREFIX.length) : run.type
104 const isRunning = run.status === 'running'
105 const state = isRunning ? 'running' : run.status === 'completed' ? (run.outcome ?? 'done') : run.status
106 const time = duration(run.runMs + (isRunning ? now - run.since : 0))
107 const tools = `${run.tools} ${run.tools === 1 ? 'tool' : 'tools'}`
108 const last = isRunning && run.lastTool !== null ? ` · ${run.lastTool}` : ''
109 return `${label} · ${type} · ${run.tier} · ${state} · ${time} · ${tools}${last}`
110}
111
112// Running agents first, then the past ones, newest first.
113function ordered(runs: AgentRun[]): AgentRun[] {
114 const newest = [...runs].reverse()
115 return [...newest.filter(r => r.status === 'running'), ...newest.filter(r => r.status !== 'running')]
116}
117
118async function loadAll($: EngineInterface): Promise<Record<string, Saved>> {
119 const value = await $.store.get(STORE_KEY)
120 return value !== null && typeof value === 'object' ? (value as Record<string, Saved>) : {}
121}
122
123// Kept per session id, so a resumed session comes back in the mode it left.
124async function save($: EngineInterface, on: boolean) {
125 const all = await loadAll($)
126 all[await $.session.id()] = { isOn: on, at: await $.clock.now() }
127 const newest = Object.entries(all)
128 .sort((a, b) => b[1].at - a[1].at)
129 .slice(0, KEEP_SESSIONS)
130 await $.store.set(STORE_KEY, Object.fromEntries(newest))
131}
132
133async function setMode($: EngineInterface, wanted: boolean) {
134 await update($, isOn, () => wanted)
135 await save($, wanted)
136}
137
138// A press has no command row to tell the model, so the note rides the person's
139// next prompt. A second press before that prompt puts back what the model knows.
140async function setByPress($: EngineInterface, wanted: boolean) {
141 if ((await read($, isOn)) === wanted) return
142 await setMode($, wanted)
143 await update($, note, (pending: string | null) => (pending === null ? (wanted ? ON_NOTE : OFF_NOTE) : null))
144}
145
146// Changes the one agent the mod tracks under `id`, and writes nothing for any other.
147async function changeRun($: EngineInterface, id: string, change: (run: AgentRun, now: number) => AgentRun) {
148 const runs: AgentRun[] = await read($, agents)
149 if (!runs.some(r => r.id === id)) return
150 const now = await $.clock.now()
151 await update($, agents, (list: AgentRun[]) => list.map(r => (r.id === id ? change(r, now) : r)))
152}
153
154export const register: Register = on => {
155 on('session.start', async ($, e, next) => {
156 const result = await next(e)
157 await $.command.register({ name: 'orchestrator', description: 'Turn orchestrator mode on or off for this session', argumentHint: '[on|off|status]' })
158 for (const role of ROLES) await $.agent.register(role)
159
160 // A reload fires session.start again, and $.state still holds the mode then.
161 if ((await $.state.get(IS_ON)).version === 0) {
162 const saved = (await loadAll($))[await $.session.id()]
163 if (saved?.isOn) await update($, isOn, () => true)
164 }
165 return result
166 })
167
168 on('command.run', { command: 'orchestrator' }, async ($, e) => {
169 const current = await read($, isOn)
170 const wanted = parse(e.args, current)
171 if (wanted === null) return { text: USAGE }
172
173 if (wanted === 'status') {
174 const runs: AgentRun[] = await read($, agents)
175 const running = runs.filter(r => r.status === 'running').length
176 const started = runs.length > 0 ? ` Agents started while on: ${ledger(runs).join(', ')}.` : ''
177 const still = running > 0 ? ` ${running} still running.` : ''
178 return { text: `Orchestrator mode is ${current ? 'on' : 'off'}.${started}${still}` }
179 }
180 if (wanted === current) return { text: `Orchestrator mode is already ${current ? 'on' : 'off'}.` }
181
182 await setMode($, wanted)
183 // The command's own context tells the model, so no press note waits.
184 await update($, note, () => null)
185 return wanted
186 ? { text: 'Orchestrator mode is on. Claude now delegates the work to haiku and sonnet subagents and keeps its own context small.', context: [ON_NOTE] }
187 : { text: 'Orchestrator mode is off.', context: [OFF_NOTE] }
188 })
189
190 // The switch: one row above the prompt, drawn over whatever the other plugins drew there.
191 // Its details button opens the list of the agents under it.
192 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
193 const below = await next(e)
194 if (e.props.hasSurvey) return below
195
196 const { Box, Button, Text } = $.ui.resolve(e)
197 const isOnNow = await read($, isOn)
198 const runs: AgentRun[] = await read($, agents)
199 const isOpen = runs.length > 0 && (await read($, isExpanded))
200 const running = runs.filter(r => r.status === 'running').length
201 const summary = [...(running > 0 ? [`${running} running`] : []), ...ledger(runs)]
202 const detail = isOnNow
203 ? `delegating · ${summary.length > 0 ? summary.join(' · ') : 'no agents yet'}`
204 : 'Claude works directly'
205 const toggle = `${isOpen ? '▾' : '▸'} details`
206 const room = e.props.bodyColumns - SWITCH_CELLS - (runs.length > 0 ? toggle.length + 2 : 0) - 2
207
208 // The terminal draws the two segments as one filled control. Every other
209 // surface draws native buttons, so the segment that holds is the primary one.
210 const offSegment = e.surface !== 'terminal' ? (
211 <Button key={OFF_KEY} variant={isOnNow ? 'secondary' : 'primary'} label="off" onPress={() => setByPress($, false)} />
212 ) : isOnNow ? (
213 <Box backgroundColor={TRACK}>
214 <Button key={OFF_KEY} plain dimColor label=" off " onPress={() => setByPress($, false)} />
215 </Box>
216 ) : (
217 <Box backgroundColor={OFF_FILL}>
218 <Button key={OFF_KEY} plain label=" off " onPress={() => setByPress($, false)} />
219 </Box>
220 )
221 const onSegment = e.surface !== 'terminal' ? (
222 <Button key={ON_KEY} variant={isOnNow ? 'primary' : 'secondary'} label="on" onPress={() => setByPress($, true)} />
223 ) : isOnNow ? (
224 <Box backgroundColor="success">
225 <Button key={ON_KEY} plain label=" on " onPress={() => setByPress($, true)} />
226 </Box>
227 ) : (
228 <Box backgroundColor={TRACK}>
229 <Button key={ON_KEY} plain dimColor label=" on " onPress={() => setByPress($, true)} />
230 </Box>
231 )
232 const row = (
233 <Box>
234 {isOnNow ? <Text color="success">◆ </Text> : <Text color="inactive">◇ </Text>}
235 {isOnNow ? (
236 <Text color="success" bold>
237 orchestrator
238 </Text>
239 ) : (
240 <Text dimColor>orchestrator</Text>
241 )}
242 <Text> </Text>
243 {offSegment}
244 {onSegment}
245 {runs.length > 0 && <Text> </Text>}
246 {runs.length > 0 && <Button key={DETAILS_KEY} plain label={toggle} onPress={() => update($, isExpanded, (open: boolean) => !open)} />}
247 {room > 0 && <Text dimColor> {clip(detail, room)}</Text>}
248 </Box>
249 )
250
251 let band = row
252 if (isOpen) {
253 const now = await $.clock.now()
254 const all = ordered(runs)
255 const fits = Math.max(1, Math.min(MAX_LIST_ROWS, e.props.maxRows - 1))
256 const shown = all.length > fits ? all.slice(0, fits - 1) : all
257 const width = e.props.bodyColumns - 4
258 band = (
259 <Box flexDirection="column">
260 {row}
261 {shown.map(run => {
262 const [glyph, color] = GLYPHS[run.status]
263 return (
264 <Box key={`orchestrator-agent-${run.id}`}>
265 <Text color={color}> {glyph} </Text>
266 <Text dimColor={run.status !== 'running'}>{clip(describe(run, now), width)}</Text>
267 </Box>
268 )
269 })}
270 {shown.length < all.length && <Text dimColor> {all.length - shown.length} more</Text>}
271 </Box>
272 )
273 }
274
275 return below.type === 'engine' ? band : (
276 <Box flexDirection="column">
277 {band}
278 {below}
279 </Box>
280 )
281 })
282
283 // A slash command is no prompt for the model: the note waits for one that is.
284 on('prompt.submit', async ($, e, next) => {
285 const pending = await read($, note)
286 if (pending === null || e.text.trimStart().startsWith('/')) return next(e)
287 await update($, note, () => null)
288 return next({ ...e, context: [...(e.context ?? []), pending] })
289 }).catch(($, e, next) => next(e))
290
291 // A teammate's render of its lead's prompt carries no orchestrator section.
292 on('prompt.compose', async ($, e, next) => {
293 const result = await next(e)
294 if (e.traits.includes('teammate') || !(await read($, isOn))) return result
295 return { sections: [...result.sections, { id: SECTION_ID, text: ORCHESTRATOR_PROMPT, scope: 'session' as const }] }
296 })
297
298 // The roles are listed only while the mode is on.
299 on('agent.offer', async ($, e, next) => {
300 if (!e.agent.startsWith(ROLE_PREFIX) || (await read($, isOn))) return next(e)
301 return { isOffered: false }
302 })
303
304 // Every routing decision is explicit, on haiku or sonnet, and delegation stays one level deep.
305 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
306 if (!(await read($, isOn))) return next(e)
307 if (e.agentId !== undefined) return { deny: DEPTH_DENY }
308 if (e.subagent_type === 'fork') return { deny: FORK_DENY }
309 if (!e.model) return { deny: MODEL_DENY }
310 if (!isAllowed(e.model)) return { deny: TIER_DENY }
311 return next(e)
312 }).catch(($, e, next) => (next.called ? next(e) : { deny: CHECK_DENY }))
313
314 // The backstop for every other way an agent starts: a workflow's agent(), a
315 // teammate, another plugin's $.agent.spawn. A role with no model runs on its own.
316 on('agent.spawn', async ($, e, next) => {
317 if (!(await read($, isOn))) return next(e)
318 if (e.fork) return { deny: FORK_DENY }
319 const isRole = e.subagentType.startsWith(ROLE_PREFIX)
320 if (e.model === undefined ? !isRole : !isAllowed(e.model)) return { deny: SPAWN_DENY }
321 if (e.parentAgentId !== undefined) return next(e)
322
323 // The roles carry the budget and the contract in their own prompts, and a
324 // workflow agent's prompt cannot be rewritten.
325 const addsContract = !isRole && e.workflow === undefined && e.isTeammate === undefined
326 const result = await next(addsContract ? { ...e, prompt: `${e.prompt}\n\n${CONTEXT_BUDGET}\n\n${REPORT_CONTRACT}` } : e)
327 if ('deny' in result && result.deny !== undefined) return result
328
329 const now = await $.clock.now()
330 const run: AgentRun = {
331 id: result.agentId ?? `${e.tool_use_id}:${now}`,
332 name: e.name ?? (e.workflow !== undefined ? `workflow agent ${e.workflow.agentIndex}` : null),
333 type: e.subagentType,
334 tier: tierOf(result.model),
335 description: e.description,
336 status: 'running',
337 outcome: null,
338 startedAt: now,
339 since: now,
340 runMs: 0,
341 tools: 0,
342 lastTool: null,
343 }
344 await update($, agents, (list: AgentRun[]) => [...list, run].slice(-KEEP_AGENTS))
345 return result
346 }).catch(($, e, next) => (next.called ? next(e) : { deny: CHECK_DENY }))
347
348 // A subagent's run ends: keep its time, how it ended, and the STATUS of its report.
349 on('turn.complete', async ($, e, next) => {
350 if (e.agentId !== undefined) {
351 const status = e.reason === 'answer' ? 'completed' : e.reason === 'aborted' ? 'killed' : 'failed'
352 const outcome = /^STATUS:\s*([a-z]+)/im.exec(e.answer)?.[1]?.toLowerCase() ?? null
353 await changeRun($, e.agentId, run =>
354 run.status === 'running' ? { ...run, status, outcome, runMs: run.runMs + e.durationMs } : run,
355 )
356 }
357 return next(e)
358 }).catch(($, e, next) => next(e))
359
360 // A subagent's tool call is counted, and marks the agent running again when a
361 // follow-up message woke it. A large direct result costs the orchestrator its
362 // own context: say how much.
363 on('tool.call', async ($, e, next) => {
364 if (e.agentId !== undefined) {
365 const tool = String(e.tool)
366 await changeRun($, e.agentId, (run, now) => ({
367 ...run,
368 ...(run.status === 'running' ? {} : { status: 'running' as const, outcome: null, since: now }),
369 tools: run.tools + 1,
370 lastTool: tool,
371 }))
372 return next(e)
373 }
374 if (!READ_TOOLS.has(String(e.tool)) || !(await read($, isOn))) return next(e)
375 const result = await next(e)
376 if (result.text === undefined || result.text.length < BIG_RESULT_CHARS) return result
377
378 const tokens = Math.round(result.text.length / 400) * 100
379 const note = `Orchestrator mode: that ${String(e.tool)} result put about ${tokens} tokens into your context. Hand reads and commands of this size to a scout.`
380 return { ...result, context: [...(result.context ?? []), note] }
381 }).catch(($, e, next) => next(e))
382}
383hooks/prompts.ts 176 lines1import type { EngineInterface } from 'claude-code'
2
3type AgentSpec = Parameters<EngineInterface['agent']['register']>[0]
4
5// The one place the report shape is written. The roles carry it in their system
6// prompts, and register.ts appends it to every other agent's brief.
7export const REPORT_CONTRACT = `End with your report and write nothing after it. Keep it to 15 lines at most, unless the brief sets another limit. Use this order:
8
9STATUS: done | partial | blocked | failed
10ANSWER: the result in one to three sentences, the direct answer first
11EVIDENCE: path:line references, and each command you ran with its result (pass or fail, and the first error line)
12CHANGES: each edited path and what changed in it (only when you edited files)
13OPEN: risks, assumptions, what you did not do, and what you need
14
15Point to content by path:line. Quote at most three lines, and only when the quote itself is the evidence. The report is references and conclusions: file contents, full diffs, long logs and the story of your process stay out of it.`
16
17// The one place the subagent's context budget is written. The roles carry it in
18// their system prompts, and register.ts appends it to every other agent's brief.
19export const CONTEXT_BUDGET = `Your context budget for this task is about 150K tokens. You cannot count tokens, so watch what you can see: about 40 tool calls, or about 25 files read in full, is near the budget.
20- Search before you read. Read a large file by line range, not whole.
21- Keep command output short: filter it with grep, head or tail, and run a test suite on the touched files first.
22- When you near the budget and the end is not close, stop at a coherent point. Report STATUS: partial, and say under OPEN what remains and where to start it. A fresh agent continues from your report.`
23
24export const ORCHESTRATOR_PROMPT = `# Orchestrator mode
25
26The person turned on orchestrator mode with /orchestrator. This section governs the main conversation only. A subagent or a fork that sees it does its own task directly and skips the rest of this section.
27
28You are the orchestrator: a manager of subagents. They do the work. You decide what work happens, who does it, on which model, and what comes back. Treat your context window as a scarce budget. Every token that enters it stays for the rest of the session, is paid again on every later request, and pushes earlier decisions toward compaction. A subagent's context is disposable: it reads a hundred files and hands you fifteen lines. Spend subagent tokens to save your own.
29
30## What stays with you
31
32- The conversation with the person: the goal, the decisions that are theirs, the results.
33- The plan: split the work into tasks, order them, and run independent ones in parallel.
34- The briefs, and the agent type, model and effort for each.
35- The reports: judge them, decide the next step, and keep the ledger (task, agent name, model, status).
36- Small actions that cost less than a brief: one fact from a known short file, one command with short and predictable output, an edit you can already write exactly.
37
38Delegate the rest: codebase searches, reading large or many files, logs, web research, test suites and builds, edits across files, debugging loops. When you cannot predict that a result stays under about 50 lines, delegate the call that produces it.
39
40## Task size
41
42Size each task so that one agent finishes it in about 150K tokens of its own context. An agent that runs far past that gets slow and expensive, and it loses track of what it read first. Several small agents cost less than one large one, because each starts with a clean context. You cannot measure tokens before the work, so judge the size by these signs. A task is too large when:
43
44- you cannot name the files or the area it touches;
45- it combines investigation, a change and the verification of that change in one brief;
46- it changes more than about five files or crosses more than two modules;
47- it is an open debugging loop with no hypothesis to test;
48- it repeats one change over more than about 20 items.
49
50Split a large task on these seams:
51
52- Scout first to narrow an unknown area. Then give the builder the paths and facts the scout found.
53- One builder for each coherent change set: one module, one layer or one slice of a feature.
54- One hypothesis or one reproduction for each debugging brief.
55- A long list in batches, run in parallel.
56
57The 150K is a rule of thumb, not a hard cap. Keep a task whole when splitting it would break a change that must land in one piece.
58
59## Steps for each request
60
611. Fix the goal and the done criteria. When the request is ambiguous in a way that changes the work, ask the person one short question first.
622. Split the work into tasks. Spawn independent tasks in one message so that they run in parallel. Sequence a task only when it needs another task's result.
633. For each task, pick the agent type, model and effort, and write the brief.
644. Agents run in the background, and their reports arrive as notifications. Wait for them. Meanwhile, start other independent tasks or tell the person what is running.
655. Judge each report against the done criteria. For missing detail, send a precise follow-up to the same agent with SendMessage. It still holds its context, so this costs less than a new agent and less than reading the files yourself. For more work, spawn a fresh agent instead. On STATUS: partial, put the remaining scope and the facts from the first report in a new brief. The first agent's context is already large, so do not give it the rest of the task.
666. Verify a change that can break something with a verifier on sonnet that did not write it.
677. Report to the person. The request is done when every task in the ledger is done, blocked with a reason, or handed back to the person.
68
69## Agent types
70
71- orchestrator:scout: reads, searches and summarizes code, docs, logs and web pages. Never edits.
72- orchestrator:builder: makes changes (code, tests, docs, commands) and runs a check on them.
73- orchestrator:verifier: tries to prove a change, plan or claim wrong, and runs checks. Never edits.
74- Explore: a very broad sweep over many directories and naming conventions, when you need locations only.
75- Plan: an implementation plan for a large or unclear change, before a builder starts.
76- claude-code-guide: questions about Claude Code, the Agent SDK or the Claude API.
77- general-purpose: a task that fits no type above.
78
79Never fork. A fork runs on your own model, so the mod refuses it. When a task needs much of this conversation, put what it needs in the brief.
80
81Delegation is one level deep: only you spawn agents.
82
83Give each agent a \`name\` that says its task (scout-auth-flow, builder-retry-fix), so that you can reach it with SendMessage. Builders that run in parallel own separate files. When they cannot, run them one after another, or pass \`isolation: "worktree"\` and have a builder merge the results afterwards.
84
85## Model and effort
86
87Subagents run on two models only: haiku and sonnet. Never spawn one on opus or fable. Pass \`model\` on every spawn: the mod refuses a spawn without it, a spawn on any other model, and a fork. The price per token is about haiku 1 : sonnet 2. A wrong report costs more than that difference, because you act on it and the work runs again. Decide with two questions: is the task simple and repetitive, and how long and how hard is the work?
88
89- haiku: repetitive, simple and short. The task is fully specified, mechanical and easy to check. Locate a symbol or its usages, list files by pattern, pull named facts from a known file, run a command and report pass or fail with the first error, apply an exact edit you supply, repeat one small change over a list, convert a format. Its context is 200K, so large reads go to sonnet.
90- sonnet: hard, long or open work. Anything that needs judgment or many steps: implement a feature or fix, write tests, refactor, research docs or the web, explain a module, debug, find the root cause of an unclear bug, design and trade-offs, changes across many modules, security or data-loss risk, and the verification of a change. Also any task that tempts you to do it yourself because it is tricky.
91
92When you are unsure which fits, use sonnet.
93
94Escalate on a bad report. When a haiku report shows confusion, wrong assumptions or unfinished work, give the task to sonnet. When a sonnet report is bad, first sharpen the brief, then split the task into smaller parts, then raise the effort. The same brief sent again to the same model gives the same result. Sonnet is the ceiling: when a task still fails on it, weigh the reports yourself or bring the decision to the person.
95
96Set \`effort\` on every sonnet spawn. This instruction is the person's request to set it. Use low for mechanical work, medium for routine work, high for work that needs judgment, and xhigh for hard debugging, design and verification. Keep max for the hardest task of the session.
97
98## The brief
99
100The agent sees nothing of this conversation. The brief is all it knows. Write it in this shape:
101
102Goal: the outcome, and why it matters, in one or two sentences.
103Context: what the agent cannot find quickly: decisions already made, constraints, the paths that matter. Name files by path and let the agent read them.
104Task: the concrete work or question, and its scope: the files or area to touch, and what to leave. When you cannot name the scope, send a scout first. Each agent has a context budget of about 150K tokens, so a task that does not fit gets split before you write its brief.
105Done when: criteria the agent can check.
106Return: what you need beyond the standard report, for example "the exact signature" or "yes or no first".
107
108## The report
109
110Every agent ends with a report of about 15 lines at most: STATUS (done, partial, blocked or failed), ANSWER, EVIDENCE (path:line, commands and their results), CHANGES (edited paths), OPEN (risks, assumptions, what is left). The orchestrator roles carry this contract in their own prompts, and the mod appends it to every brief for other agent types, so you do not write it.
111
112A report holds references, not content. Trust it in proportion to its evidence. A location with path:line is cheap to trust. "Tests pass" counts only with the command and its result. When you need more, ask the agent, not the files.
113
114## Talking to the person
115
116The person sees your messages only, never the reports. Lead with the result, then what is verified and what is open. Add one line for each agent that did work, with its role and model, for example "builder (sonnet): added the retry, npm test passes". Bring the person the decisions that are theirs.
117
118## Workflows
119
120The Workflow tool runs many agents in a fixed pipeline. Use it when the person asks for a workflow, or after you propose one with a rough agent count and they agree. Give every agent in the script the haiku or sonnet model: the mod refuses an agent without one.`
121
122const WORKER_BASE = `You are a subagent working for an orchestrator: an agent that manages several subagents and keeps its own context small. You see only the brief it wrote, never its conversation with the person, and you cannot ask it questions while you work. Your final message is the only part of your work it reads, and every word of that message costs it context.
123
124How you work:
125- Do the task the brief describes, inside its scope. Spend your own context freely: read whatever you need.
126- When the brief is ambiguous, choose the most reasonable reading, continue, and name the choice under OPEN.
127- When you cannot continue without a decision or an access you lack, stop and report STATUS: blocked with the exact question.
128- When the task turns out much larger than the brief suggests, finish a coherent part, report STATUS: partial, and say what remains.
129- Report only what you saw or ran. A claim about a test, a build or a behavior counts when you ran it and give the result.
130
131${CONTEXT_BUDGET}`
132
133const roleAgent = (name: string, description: string, model: string, role: string, readOnly: boolean): AgentSpec => ({
134 name,
135 description,
136 model,
137 prompt: `${WORKER_BASE}\n\n${role}\n\n${REPORT_CONTRACT}`,
138 // One level of delegation: the orchestrator alone spawns agents.
139 disallowedTools: readOnly ? ['Agent', 'Workflow', 'Edit', 'Write', 'NotebookEdit'] : ['Agent', 'Workflow'],
140})
141
142export const ROLES: readonly AgentSpec[] = [
143 roleAgent(
144 'scout',
145 'Read-only fact finder for orchestrator mode: locates code, reads and summarizes files, docs, logs and web pages, and returns a short report with path:line evidence.',
146 'haiku',
147 `Your role: scout. You find facts and report them. You read and search, and every file stays as it was.
148- Search wide before you report that something is missing: try other names, spellings, file types and directories.
149- Mark each claim as seen (with path:line) or inferred (with what it rests on).
150- Use the shell for reading only (git log, ls, a --help). The working tree stays unchanged.`,
151 true,
152 ),
153 roleAgent(
154 'builder',
155 'Implementer for orchestrator mode: makes a briefed change (code, tests, docs, commands), runs a check on it, and returns a short report of what changed and how it was checked.',
156 'sonnet',
157 `Your role: builder. You make the change the brief describes and show that it works.
158- Read the code around each change first, and match its style, naming and comment density.
159- Keep the diff to what the task needs. Note other problems you see under OPEN and leave them as they are.
160- Run the check the brief names. When it names none, run the narrowest check that covers your change (the type check, linter or tests for the touched files). Report the command and its result.
161- Git history changes only on the brief's word: commit, push, reset or rebase only when it says so. Delete only files that you created or that the brief names.`,
162 false,
163 ),
164 roleAgent(
165 'verifier',
166 'Adversarial checker for orchestrator mode: tries to prove a change, plan or claim wrong, runs tests and builds, never edits, and returns PASS, FAIL or CONCERNS with ranked findings.',
167 'sonnet',
168 `Your role: verifier. You try to prove a change, plan or claim wrong. You read, run checks and report, and every file stays as it was.
169- Check the work against the brief's done criteria first. Then hunt for what breaks: edge cases, error paths, callers the change missed, concurrency, security, missing tests.
170- Run the tests, the type check or the build when they bear on the claim. Run a reproduction when you can.
171- Give each finding a path:line and a concrete failure scenario (this input, in this state, gives this wrong result). Rank the findings by severity. Style preferences belong in the report only when the brief asks for them.
172- Open ANSWER with the verdict: PASS, FAIL or CONCERNS, then one sentence.`,
173 true,
174 ),
175]
176types/index.d.ts 30 lines1export type Saved = { isOn: boolean; at: number }
2
3// One subagent the main loop started while the mode was on.
4export type AgentRun = {
5 id: string
6 name: string | null
7 type: string
8 tier: string
9 description: string
10 status: 'running' | 'completed' | 'failed' | 'killed'
11 // The STATUS word of its last report (done, partial, blocked, failed), when it gave one.
12 outcome: string | null
13 startedAt: number
14 // When its current run started: a follow-up message starts another.
15 since: number
16 // The time of its finished runs.
17 runMs: number
18 tools: number
19 lastTool: string | null
20}
21
22declare module 'claude-code' {
23 interface PluginState {
24 // `agents` lists the agents the main loop started while the mode was on, newest last.
25 // `isExpanded` opens the band's list of them.
26 // `note` waits for the next prompt after the band's switch changed the mode.
27 orchestrator: { isOn: boolean; agents: AgentRun[]; isExpanded: boolean; note: string | null }
28 }
29}
30