An OpenSwarm-style team: /swarm hands one request to specialist agents (research, data analysis, slides, documents, images, video), shows their progress live…

An OpenSwarm-style team for Claude Code, modelled on VRSEN/OpenSwarm. Type one request: Claude plans it, hands the work to specialist agents (in parallel where it can), checks every deliverable, and collects them in one folder with a gallery page.
/swarm a briefing on US home battery storage for homeowners: a 7-slide deck, a one-page executive summary, a price-trend chart and a 20-second teaser video
swarm-output/2026-10-11-home-batteries/
index.html ← open this: every deliverable with previews
manifest.json what was asked, who made what, anything that needs you
research.md cited research brief
charts/*.png themed charts analysis/ findings + the script
slides/deck.html present in a browser (arrow keys, F for full screen)
slides/deck.pptx PowerPoint, with speaker notes
slides/deck.pdf
docs/*.docx *.pdf Word and PDF
images/*.png illustrations
video/video.mp4 slideshow video (+ .srt captions)
Everything in one run shares a theme: ocean (default), paper, midnight, forest or ember.
| Agent | Makes | Needs |
|---|---|---|
researcher | Cited research brief with a key-figures table | — |
data-analyst | Charts styled to the theme, findings, the analysis script | — |
slides-designer | HTML deck + PPTX + PDF, checked slide by slide | — |
docs-writer | Word + PDF documents, checked by eye | — |
image-maker | AI images with a key; otherwise clean drawn SVG/PNG illustrations | OPENROUTER_API_KEY for AI images |
video-maker | Slideshow/explainer videos with pans, crossfades, captions, music; clip edits | ffmpeg; FAL_KEY / GOOGLE_API_KEY / OPENAI_API_KEY for AI footage |
PDFs and previews are rendered with Microsoft Edge or Chrome, headless.
A Swarm panel opens when the first specialist starts: one row per specialist, colour-coded, with its task, elapsed time, step count and what it is doing right now. c clears finished rows; /swarm-panel reopens it.
skills/swarm/SKILL.md — the orchestrator: plan, waves, briefs, quality checks, gallery.agents/*.md — the six specialists.skills/swarm/kit/ — swarm_kit, the shared builders (decks, documents, videos, previews, gallery, doctor). Installed into its own Python environment at ~/.claude/swarm/.venv by skills/swarm/scripts/setup.py, which /swarm runs on every call (it does nothing when already current).hooks/register.tsx — the progress panel.Check what this machine can do:
python skills/swarm/scripts/setup.py
~/.claude/swarm/.venv/Scripts/python -m swarm_kit doctor
/plugin marketplace add C:\Users\Admin\Documents\Claude\Mods\swarm
/plugin install swarm@swarm-mods
claude plugin validate .
claude plugin test . # the panel
~/.claude/swarm/.venv/Scripts/python skills/swarm/kit/tests/test_kit.py # every builderhooks/register.tsx 196 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { RunStatus, SwarmRun } from '../types'
5
6const PANE = 'swarm'
7const TITLE = 'Swarm'
8const PREFIX = 'swarm:'
9
10const runs = atom({ plugin: 'swarm', key: 'runs' } as const, [] as SwarmRun[])
11const tick = atom({ plugin: 'swarm', key: 'tick' } as const, 0)
12
13// Each specialist keeps one colour, so a glance tells who is doing what.
14const AGENT_STYLE: Record<string, { label: string; color: string }> = {
15 researcher: { label: 'Research', color: 'suggestion' },
16 'data-analyst': { label: 'Data', color: 'merged' },
17 'slides-designer': { label: 'Slides', color: 'claude' },
18 'docs-writer': { label: 'Docs', color: 'planMode' },
19 'image-maker': { label: 'Images', color: 'autoAccept' },
20 'video-maker': { label: 'Video', color: 'warning' },
21}
22const agentStyle = (agent: string) => AGENT_STYLE[agent] ?? { label: agent, color: 'subtle' }
23
24const STATUS_STYLE: Record<RunStatus, { glyph: string; color: string }> = {
25 running: { glyph: '●', color: 'warning' },
26 done: { glyph: '✓', color: 'success' },
27 failed: { glyph: '✗', color: 'error' },
28}
29
30const basename = (path: string) => path.split(/[\\/]/).filter(Boolean).pop() ?? path
31
32/** A few words for what a tool call does, from its arguments. */
33function describe(e: Record<string, unknown>): string {
34 const tool = String(e.tool ?? '')
35 const str = (key: string) => (typeof e[key] === 'string' ? (e[key] as string) : '')
36 if (str('file_path')) return `${tool} ${basename(str('file_path'))}`
37 if (tool === 'Bash' || tool === 'PowerShell') return str('description') || str('command').slice(0, 60)
38 if (str('query')) return `${tool} "${str('query').slice(0, 50)}"`
39 if (str('url')) {
40 const host = /^[a-z]+:\/\/([^/]+)/i.exec(str('url'))?.[1]
41 return `${tool} ${host ?? str('url').slice(0, 50)}`
42 }
43 if (str('skill')) return `Skill ${str('skill')}`
44 if (str('pattern')) return `${tool} ${str('pattern').slice(0, 50)}`
45 return tool
46}
47
48function formatElapsed(ms: number): string {
49 const total = Math.max(0, Math.round(ms / 1000))
50 const m = Math.floor(total / 60)
51 const s = total % 60
52 return m > 0 ? `${m}m ${String(s).padStart(2, '0')}s` : `${s}s`
53}
54
55// Redraws elapsed times once a second while something runs; stops when all are finished.
56let ticker: Timer | undefined
57
58async function keepTicking($: EngineInterface) {
59 const anyRunning = (await read($, runs)).some(r => r.status === 'running')
60 if (anyRunning && !ticker) {
61 ticker = $.clock.every(1000, () => void onTick($))
62 }
63}
64
65async function onTick($: EngineInterface) {
66 await update($, tick, n => n + 1)
67 const list = await read($, runs)
68 if (!list.some(r => r.status === 'running')) {
69 ticker?.cancel()
70 ticker = undefined
71 }
72}
73
74async function patch($: EngineInterface, match: (r: SwarmRun) => boolean, fn: (r: SwarmRun) => SwarmRun) {
75 await update($, runs, list => list.map(r => (match(r) ? fn(r) : r)))
76}
77
78export const register: Register = on => {
79 on('session.start', async ($, e, next) => {
80 const result = await next(e)
81 await $.command.register({ name: 'swarm-panel', description: 'Show the swarm progress panel' })
82 return result
83 })
84
85 on('command.run', { command: 'swarm-panel' }, async $ => {
86 await $.ui.open({ id: PANE, title: TITLE })
87 return { text: 'Swarm panel opened.' }
88 })
89
90 // A specialist starts: add its row and bring the panel up.
91 on('agent.spawn', async ($, e, next) => {
92 if (!e.subagentType.startsWith(PREFIX)) return next(e)
93 const run: SwarmRun = {
94 id: e.tool_use_id,
95 agent: e.subagentType.slice(PREFIX.length),
96 description: e.description,
97 status: 'running',
98 startedAt: await $.clock.now(),
99 tools: 0,
100 lastAction: 'starting',
101 }
102 await update($, runs, list => [...list.filter(r => r.id !== run.id), run].slice(-50))
103 void $.ui.open({ id: PANE, title: TITLE })
104 await keepTicking($)
105 const started = await next(e)
106 if ('deny' in started && started.deny !== undefined) {
107 const at = await $.clock.now()
108 await patch($, r => r.id === run.id, r => ({ ...r, status: 'failed', endedAt: at, lastAction: started.deny ?? 'refused' }))
109 } else if (started.agentId) {
110 await patch($, r => r.id === run.id, r => ({ ...r, agentId: started.agentId }))
111 }
112 return started
113 }).catch((_$, e, next) => next(e))
114
115 // Its tool calls: count them and show the latest.
116 on('tool.call', async ($, e, next) => {
117 if (e.agentId) {
118 const list = await read($, runs)
119 if (list.some(r => r.agentId === e.agentId)) {
120 const action = describe(e as unknown as Record<string, unknown>)
121 await patch($, r => r.agentId === e.agentId, r => ({ ...r, tools: r.tools + 1, lastAction: action, status: 'running', endedAt: undefined }))
122 await keepTicking($)
123 }
124 }
125 return next(e)
126 }).catch((_$, e, next) => next(e))
127
128 // Its loop ends: done, or failed with the reason.
129 on('turn.complete', async ($, e, next) => {
130 const result = await next(e)
131 if (e.agentId) {
132 const status: RunStatus = e.reason === 'answer' ? 'done' : 'failed'
133 const at = await $.clock.now()
134 const why = e.reason === 'answer' ? 'finished' : e.reason
135 await patch($, r => r.agentId === e.agentId, r => ({ ...r, status, endedAt: at, lastAction: status === 'done' ? r.lastAction : why }))
136 }
137 return result
138 }).catch((_$, e, next) => next(e))
139
140 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
141 const { Box, Text, Button } = $.ui.resolve(e)
142 const list = await read($, runs)
143 await read($, tick)
144 const now = await $.clock.now()
145 const counts = { running: 0, done: 0, failed: 0 }
146 for (const r of list) counts[r.status]++
147 const room = Math.max(1, Math.floor(((e.viewport?.rows ?? 30) - 4) / 3))
148
149 return (
150 <Box flexDirection="column" gap={1}>
151 <Box flexDirection="row" gap={2} flexWrap="wrap" alignItems="center">
152 <Text color="warning">● {counts.running} running</Text>
153 <Text color="success">✓ {counts.done} done</Text>
154 <Text color={counts.failed > 0 ? 'error' : 'inactive'}>✗ {counts.failed} failed</Text>
155 {counts.done + counts.failed > 0 && (
156 <Button
157 key="clear"
158 label="Clear finished"
159 hotkey="c"
160 dimColor
161 onPress={() => update($, runs, all => all.filter(r => r.status === 'running'))}
162 />
163 )}
164 </Box>
165 {list.length === 0 && <Text dimColor>No swarm agents yet. Run /swarm with what you want made.</Text>}
166 {list.slice(-room).map(r => {
167 const who = agentStyle(r.agent)
168 const st = STATUS_STYLE[r.status]
169 const elapsed = formatElapsed((r.endedAt ?? now) - r.startedAt)
170 return (
171 <Box key={r.id} flexDirection="column">
172 <Box flexDirection="row" gap={1}>
173 <Text color={st.color} bold>
174 {st.glyph}
175 </Text>
176 <Text color={who.color} bold>
177 {who.label}
178 </Text>
179 <Text wrap="truncate-end">{r.description}</Text>
180 </Box>
181 <Box flexDirection="row" gap={1} paddingLeft={2}>
182 <Text dimColor>
183 {elapsed} · {r.tools === 1 ? '1 step' : `${r.tools} steps`} ·
184 </Text>
185 <Text color={r.status === 'failed' ? 'error' : undefined} dimColor={r.status !== 'failed'} wrap="truncate-end">
186 {r.lastAction}
187 </Text>
188 </Box>
189 </Box>
190 )
191 })}
192 </Box>
193 )
194 })
195}
196types/index.d.ts 30 lines1export type RunStatus = 'running' | 'done' | 'failed'
2
3export type SwarmRun = {
4 /** The Agent tool call that started it. */
5 id: string
6 /** The subagent's loop id, once started; its tool calls carry it. */
7 agentId?: string
8 /** The specialist, without the plugin prefix: `researcher`, `slides-designer`, ... */
9 agent: string
10 /** The Agent call's short description of the task. */
11 description: string
12 status: RunStatus
13 startedAt: number
14 endedAt?: number
15 /** Tool calls the specialist has made so far. */
16 tools: number
17 /** What it did last, e.g. `Write slides/deck.json`. */
18 lastAction: string
19}
20
21declare module 'claude-code' {
22 interface PluginState {
23 swarm: {
24 runs: SwarmRun[]
25 /** Bumped every second while a run is going, so elapsed times redraw. */
26 tick: number
27 }
28 }
29}
30