Live pane of driver-seat cheap workers: running tasks, cost, fail and escalation rates from workers.tsv

Claude Code mods (function-hook plugins). Each folder is one plugin; .claude-plugin/marketplace.json makes this repo a marketplace.
| Mod | What it does |
|---|---|
cheap-worker | /worker spins up Claude Code workers from a picker; /workers lists, messages and closes them. Team mode lets workers find each other (ListAgents) and talk (SendMessage). |
agent-progress | /progress: animated progress bar per agent (main, subagents, workers) from their task lists. |
agent-map | /map: project file tree; red 🔨 = an agent is editing it, green ✓ = done, ready for review. |
context-bar | Context-window breakdown above the prompt. |
usage-meter | 5-hour / weekly usage in the footer. |
quick-settings | ⚙ footer button for model and effort. |
skins | /skins: colour themes (Tokyo Night, Dracula, Gruvbox, …). |
cw-pane, driver-seat-guard, evidence-band | For the driver-seat workflow; skip them elsewhere. |
/plugin marketplace add ianwilson97/claude-mods
/plugin install cheap-worker@ianwilson-mods
/plugin install agent-progress@ianwilson-mods
/plugin install agent-map@ianwilson-mods
On the machine where this folder is the live copy (loaded through CLAUDE_CODE_PLUGIN_DIRS), don't also install from the marketplace: each mod would load twice.
Set per machine in /config (or pluginConfigs.cheap-worker.options in ~/.claude/settings.json):
| Option | Values | |
|---|---|---|
provider | openrouter (default) | Any OpenRouter model; key read from $CW_OPENROUTER_API_KEY (export it in ~/.zshenv). Workers skip user settings, hooks and plugins. |
bedrock | Claude only (Sonnet / Opus / Haiku). Workers keep your user settings, so the Bedrock env there (CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_PROFILE, …) applies. No call to openrouter.ai. | |
launch | panes (default) | All workers tiled in tmux session cw: tmux new -A -s cw. |
tabs | Each worker gets a tmux session of its own and opens in a background tab: a cmux tab when Claude Code runs inside cmux, else a Ghostty tab (focus stays where you were). |
Work laptop: provider = bedrock, launch = tabs.
crewThe mod installs crew into ~/.local/bin (that folder must be on your PATH; it never overwrites a different crew).
crew # list running workers, pick one by number
crew ab12c # attach to cw-ab12c
If Ghostty can't be scripted (macOS asks once to let Claude control Ghostty; a managed Mac may refuse), the worker still runs: press CMD+T and run crew. Closing a tab only detaches; the worker keeps running until you close it from /workers.
Needs: tmux; for auto-opened tabs, cmux or Ghostty 1.3+.
/plugin lists the mods as enabled. Managed settings can block marketplaces or plugins; if so, nothing loads./worker, launch one with task say hi; a Ghostty tab opens (or the toast says CMD+T, then crew …)./workers send one a message: messaging is Claude Code's own (ListAgents / SendMessage) and works under Bedrock unless policy turns it off.hooks/register.tsx 98 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { JobStats, Row, Summary } from '../types'
5
6const PANE = 'cw'
7const DIR = '.driver-seat'
8const summary = atom({ plugin: 'cw-pane', key: 'summary' } as const, null as Summary | null)
9
10// The driver-seat rule: revisit the model split at 20%+ escalated after 10 runs of a job.
11const isHot = (j: JobStats) => j.runs >= 10 && j.escalated / j.runs >= 0.2
12
13export function summarize(tsv: string, tasks: readonly string[], results: readonly string[]): Summary {
14 const lines = tsv.trim().split('\n').slice(1).filter(Boolean)
15 const rows: Row[] = lines.map(line => {
16 const c = line.split('\t')
17 return {
18 taskId: c[1] ?? '?',
19 job: c[2] ?? '?',
20 cost: Number(c[8]) || 0,
21 verdict: c[9] ?? '?',
22 escalated: c[10] === '1',
23 }
24 })
25 const jobs = new Map<string, JobStats>()
26 for (const r of rows) {
27 const j = jobs.get(r.job) ?? { job: r.job, runs: 0, fails: 0, escalated: 0, cost: 0 }
28 jobs.set(r.job, {
29 ...j,
30 runs: j.runs + 1,
31 fails: j.fails + (r.verdict === 'fail' ? 1 : 0),
32 escalated: j.escalated + (r.escalated ? 1 : 0),
33 cost: j.cost + r.cost,
34 })
35 }
36 const done = new Set(results)
37 return {
38 running: tasks.filter(t => !done.has(t)).map(t => t.replace(/\.md$/, '')),
39 recent: rows.slice(-8).reverse(),
40 jobs: [...jobs.values()],
41 cost: rows.reduce((sum, r) => sum + r.cost, 0),
42 }
43}
44
45async function refresh($: EngineInterface) {
46 const tsv = await $.fs.read(`${DIR}/workers.tsv`).catch(() => null)
47 if (typeof tsv !== 'string') return update($, summary, () => null)
48 const names = async (d: string) =>
49 (await $.fs.list(`${DIR}/${d}`).catch(() => [])).filter(e => e.name.endsWith('.md')).map(e => e.name)
50 const next = summarize(tsv, await names('tasks'), await names('results'))
51 await update($, summary, () => next)
52 $.ui.status(next.running.length > 0 ? `cw: ${next.running.length} running` : undefined)
53}
54
55export const register: Register = on => {
56 on('session.start', async ($, e, next) => {
57 await $.command.register({ name: 'cw-pane', description: 'Show driver-seat cheap workers in a pane' })
58 await refresh($)
59 $.clock.every(5000, () => refresh($)) // ponytail: polls fs; switch to a watcher if it ever lags
60 return next(e)
61 })
62
63 on('command.run', { command: 'cw-pane' }, async $ => {
64 await refresh($)
65 await $.ui.open({ id: PANE, title: 'Cheap workers' })
66 return { text: 'Cheap workers pane opened.' }
67 })
68
69 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
70 const { Box, Text } = $.ui.resolve(e)
71 const s = await read($, summary)
72 if (s === null) return <Text dimColor>No .driver-seat/workers.tsv in this project.</Text>
73 const pct = (n: number, d: number) => (d === 0 ? '0%' : `${Math.round((100 * n) / d)}%`)
74 return (
75 <Box flexDirection="column">
76 <Text bold>Running ({s.running.length})</Text>
77 {s.running.length === 0 && <Text dimColor> none</Text>}
78 {s.running.map(t => (
79 <Text key={`run-${t}`} color="yellow"> ● {t}</Text>
80 ))}
81 <Text bold>Jobs · total ${s.cost.toFixed(2)}</Text>
82 {s.jobs.map(j => (
83 <Text key={`job-${j.job}`} color={isHot(j) ? 'red' : undefined}>
84 {' '}{j.job.padEnd(9)} {String(j.runs).padStart(3)} runs · fail {pct(j.fails, j.runs)} · esc{' '}
85 {pct(j.escalated, j.runs)} · ${j.cost.toFixed(2)}{isHot(j) ? ' ← revisit model' : ''}
86 </Text>
87 ))}
88 <Text bold>Recent</Text>
89 {s.recent.map((r, i) => (
90 <Text key={`row-${i}`} dimColor={r.verdict === 'pass'} color={r.verdict === 'fail' ? 'red' : undefined}>
91 {' '}{r.verdict.padEnd(4)} {r.taskId} ${r.cost.toFixed(3)}{r.escalated ? ' ↑esc' : ''}
92 </Text>
93 ))}
94 </Box>
95 )
96 })
97}
98types/index.d.ts 10 lines1export type Row = { taskId: string; job: string; verdict: string; escalated: boolean; cost: number }
2export type JobStats = { job: string; runs: number; fails: number; escalated: number; cost: number }
3export type Summary = { running: string[]; recent: Row[]; jobs: JobStats[]; cost: number }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'cw-pane': { summary: Summary | null }
8 }
9}
10