A pane of the sessions this one orchestrates: state, branch, commits ahead, changed files, last message, who needs you

Claude Code mods for running orchestrator and worker sessions. Built and tested on Claude Code v2.1.289.
| Mod | What it does |
|---|---|
limit-resume | When the 5-hour limit resets, sends "Continue" to the session and nudges idle workers. A dim usage tail on the prompt hint line from 80%, /limits, and a one-line usage note for Claude each turn. |
workers | /workers opens a pane with every worker: state, branch, commits ahead of main, changed files, last message, and who needs you. /workers all shows every session. |
identity-keeper | Remembers the session name (from /rename or "You are <name>" in the first prompt), restores it after a restart, and gives Claude a short role card after each compaction. /identity, /identity forget. |
proof-gate | When a worker reports done without screenshots, asks it for them. When proof arrives, sends the files to you and shows a band above the prompt with Approve and Ask changes. |
effort-gate | Denies a subagent or claude --bg session that runs Opus at xhigh or max effort unless you said so: your own next prompt (terminal or Remote Control) must mention Opus and xhigh/max, e.g. "ok opus xhigh". The OK lasts until your next prompt. Sonnet, Haiku, and the main session are not gated. Also denies a claude --bg with no --model (or --agent that sets one), since it would silently run Opus, and an Opus one with no --effort (settings could raise it). It fails closed on claude --bg text it cannot read, so a message that merely mentions claude --bg is denied too. It only sees the literal command text: variables, aliases or functions, scripts written then run, and xargs get through. |
route-ledger | Records every subagent and claude --bg launch (model, effort, agent, label, never the prompt) and, for subagents (background ones too), ok/error, duration and tokens. A relaunch with the same label in the same session marks the earlier run retried or escalated. /routing-review [days] prints launches, outcomes, retries, escalations and median tokens and time per model@effort. Background sessions are launch-only: their outcome is not visible to the mod. |
config-sync | At session start (at most every 10 minutes per device) pulls ~/.claude/shared (the claude-config repo) with --ff-only. Tells you when the pull fails, when there are unpushed local changes, or when settings.shared.json changed; /config-sync apply then runs the installer, /config-sync shows the status. Skips private CLAUDE_CONFIG_DIR setups. |
savvy-progress | A progress bar above the prompt and /agents-info, a panel of every subagent with model, context, cost and time. scout, builder and reviewer get their own crabs: a ranger with binoculars, a builder with a hammer, a reviewer in a mortarboard with a clipboard. The bar's crab is the orchestrator, a conductor with a baton. Also lists related background sessions and draws the crabs as half-block text in the terminal. Copy of johnnyvizz/claude-kit (MIT). |
cache-tax | Keeps the one-hour prompt cache warm: every session starts an 8-hour keepwarm window that pings after 50 idle minutes, and a cold send shows its rewrite cost. Changed from upstream: keepwarm is always on for 8h, and the guard warns instead of dropping the message. /keepwarm off turns it off on this device. Pings stop after 3h idle and while weekly usage is 75% or more. Pings cost usage. Copy of karanb192/cache-tax (MIT). |
claude plugin marketplace add Berkay2002/berkays-mods
claude plugin install limit-resume@berkays-mods
claude plugin install workers@berkays-mods
claude plugin install identity-keeper@berkays-mods
claude plugin install proof-gate@berkays-mods
claude plugin install effort-gate@berkays-mods
claude plugin install route-ledger@berkays-mods
claude plugin install config-sync@berkays-mods
claude plugin install savvy-progress@berkays-mods
claude plugin install cache-tax@berkays-mods
Plugins carry no version, so each commit is a new version:
claude plugin marketplace update berkays-mods
claude plugin update workers@berkays-mods # and the others
Then run /reload-plugins in open sessions.
Work against the checkout, not the installed copy:
claude --plugin-dir plugins/workers
claude plugin validate plugins/workers
(cd plugins/workers && claude plugin test)
MIT
hooks/register.tsx 321 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Contact, Worker } from '../types'
5import {
6 asksForYou,
7 bodyOf,
8 contactFor,
9 contactKey,
10 cut,
11 gitOf,
12 isWorker,
13 lastOf,
14 lookOf,
15 parseAgents,
16 senderOf,
17 sortWorkers,
18 stateOf,
19 summaryOf,
20 age,
21} from './lib'
22
23const PANE = 'workers'
24const TITLE = 'Workers'
25const EVERY_MS = 30_000
26const GONE_FOR_MS = 12 * 3600_000
27const NARROW = 50
28
29const contacts = atom({ plugin: 'workers', key: 'contacts' } as const, {})
30const rows = atom({ plugin: 'workers', key: 'rows' } as const, [])
31const others = atom({ plugin: 'workers', key: 'others' } as const, 0)
32const updatedAt = atom({ plugin: 'workers', key: 'updatedAt' } as const, 0)
33const showAll = atom({ plugin: 'workers', key: 'showAll' } as const, false)
34const error = atom({ plugin: 'workers', key: 'error' } as const, null)
35const isOpen = atom({ plugin: 'workers', key: 'isOpen' } as const, false)
36const needs = atom({ plugin: 'workers', key: 'needs' } as const, 0)
37
38async function git($: EngineInterface, cwd: string, args: string[]): Promise<string | null> {
39 try {
40 const r = await $.process.run(['git', '-C', cwd, ...args], { timeoutMs: 5000 })
41 return r.exitCode === 0 ? r.stdout.trim() : null
42 } catch {
43 return null
44 }
45}
46
47async function gitInfo($: EngineInterface, cwd: string) {
48 const branch = await git($, cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
49 if (branch === null) return { branch: null, ahead: null, dirty: null }
50 const [ahead, status] = await Promise.all([
51 git($, cwd, ['rev-list', '--count', 'main..HEAD']),
52 git($, cwd, ['status', '--porcelain']),
53 ])
54 return {
55 branch,
56 ahead: ahead === null ? null : Number(ahead),
57 dirty: status === null ? null : status.split('\n').filter(Boolean).length,
58 }
59}
60
61let tick: { cancel: () => void } | null = null
62let isRefreshing = false
63
64async function refresh($: EngineInterface): Promise<number> {
65 if (isRefreshing) return (await read($, rows)).length
66 isRefreshing = true
67 try {
68 const self = await $.session.id()
69 const repo = await $.session.repo()
70 let agentsOut: string
71 try {
72 const r = await $.process.run(['claude', 'agents', '--json'], { timeoutMs: 10_000 })
73 if (r.exitCode !== 0) throw new Error(r.stderr.trim() || `exit ${r.exitCode}`)
74 agentsOut = r.stdout
75 } catch (err) {
76 await update($, error, () => `Could not list sessions: ${cut(String(err), 100)}`)
77 await update($, updatedAt, () => Date.now())
78 return (await read($, rows)).length
79 }
80 const known = await read($, contacts)
81 const all = await read($, showAll)
82 const now = Date.now()
83 const agents = parseAgents(agentsOut).filter(a => a.sessionId !== self)
84 const picked = agents.filter(a => all || isWorker(a, known, repo?.root ?? null)).slice(0, 24)
85 const live: Worker[] = await Promise.all(
86 picked.map(async a => ({
87 name: a.name,
88 pid: a.pid,
89 sessionId: a.sessionId,
90 cwd: a.cwd,
91 state: stateOf(a.status),
92 waitingFor: a.waitingFor ?? null,
93 ...(await gitInfo($, a.cwd)),
94 })),
95 )
96 // Workers this session talked to that are no longer running.
97 const names = new Set(agents.map(a => a.name))
98 const gone: Worker[] = Object.entries(known)
99 .filter(([key, c]) => !key.startsWith('pid:') && !names.has(key) && now - c.at < GONE_FOR_MS)
100 .map(([name]) => ({
101 name,
102 pid: 0,
103 sessionId: '',
104 cwd: '',
105 state: 'gone',
106 waitingFor: null,
107 branch: null,
108 ahead: null,
109 dirty: null,
110 }))
111 const list = [...live, ...gone]
112 await update($, rows, () => list)
113 await update($, others, () => agents.length - live.length)
114 await update($, error, () => null)
115 await update($, updatedAt, () => now)
116 // Only real workers count toward the hint tail, also while the pane shows every session.
117 const workerIds = new Set(agents.filter(a => isWorker(a, known, repo?.root ?? null)).map(a => a.sessionId))
118 const isReal = (w: Worker) => w.state === 'gone' || workerIds.has(w.sessionId)
119 const waiting = list.filter(w => isReal(w) && lookOf(w, contactFor(known, w)).rank === 0).length
120 await update($, needs, () => waiting)
121 return list.length
122 } finally {
123 isRefreshing = false
124 }
125}
126
127async function openPane($: EngineInterface): Promise<void> {
128 await $.ui.open({ id: PANE, title: TITLE })
129 await update($, isOpen, () => true)
130 tick?.cancel()
131 tick = $.clock.every(EVERY_MS, () => void refresh($))
132}
133
134async function touch($: EngineInterface, key: string, contact: Contact): Promise<void> {
135 await update($, contacts, all => ({ ...all, [key]: contact }))
136 if (await read($, isOpen)) void refresh($)
137}
138
139export const register: Register = on => {
140 on('session.start', async ($, e, next) => {
141 const started = await next(e)
142 await $.command.register({
143 name: 'workers',
144 description: 'Toggle the workers pane (/workers all: every session, /workers refresh)',
145 })
146 if (await read($, isOpen)) {
147 // A reload keeps the pane but not the timer.
148 await openPane($)
149 void refresh($)
150 } else if ((await refresh($)) > 0) {
151 await openPane($)
152 }
153 return started
154 })
155
156 on('command.run', { command: 'workers' }, async ($, e) => {
157 const arg = e.args.trim()
158 if (arg === 'all') {
159 const all = !(await read($, showAll))
160 await update($, showAll, () => all)
161 await openPane($)
162 await refresh($)
163 return { text: all ? 'Workers pane shows every session' : 'Workers pane shows workers only' }
164 }
165 if (arg === 'refresh') {
166 await openPane($)
167 await refresh($)
168 return { text: 'Workers pane refreshed' }
169 }
170 if (await read($, isOpen)) {
171 await $.ui.close({ id: PANE })
172 return { text: 'Workers pane closed' }
173 }
174 await openPane($)
175 await refresh($)
176 return { text: 'Workers pane opened' }
177 })
178
179 on('ui.close', { id: PANE }, async ($, e, next) => {
180 tick?.cancel()
181 tick = null
182 await update($, isOpen, () => false)
183 return next(e)
184 })
185
186 on('session.send', async ($, e, next) => {
187 const sent = await next(e)
188 if (e.agentId === undefined && sent.isDelivered) {
189 await touch($, contactKey(e.to), {
190 text: bodyOf(e.text).slice(0, 200),
191 at: Date.now(),
192 dir: 'out',
193 needsYou: false,
194 })
195 }
196 return sent
197 })
198
199 on('session.receive', async ($, e, next) => {
200 const received = await next(e)
201 const isPeer = e.origin.kind === 'peer' || e.origin.kind === 'peer-send-message'
202 const from = isPeer && e.agentId === undefined ? senderOf(e.text) : null
203 if (from !== null) {
204 const body = bodyOf(e.text)
205 await touch($, from, { text: body.slice(0, 200), at: Date.now(), dir: 'in', needsYou: asksForYou(body) })
206 }
207 return received
208 })
209
210 // Workers waiting on the person ride the hint line as a dim tail, after any tail another hook already set.
211 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
212 const n = await read($, needs)
213 if (n <= 0) return next(e)
214 const line = `${n} worker${n === 1 ? '' : 's'} waiting`
215 const tail = e.props.tail ? `${e.props.tail} · ${line}` : line
216 return next({ ...e, props: { ...e.props, tail } })
217 })
218
219 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
220 const { Box, Text, Button } = $.ui.resolve(e)
221 const known = await read($, contacts)
222 const list = sortWorkers(await read($, rows), known)
223 const at = await read($, updatedAt)
224 const failed = await read($, error)
225 const rest = await read($, others)
226 const all = await read($, showAll)
227 const now = Date.now()
228 const width = Math.max(20, e.props.bodyColumns)
229 const isNarrow = width < NARROW
230
231 const footer = (
232 <Box flexDirection="row" gap={2} marginTop={1}>
233 <Button key="refresh" plain hotkey="r" label="Refresh" onPress={() => void refresh($)} />
234 <Button
235 key="all"
236 plain
237 hotkey="a"
238 dimColor
239 label={all ? 'Workers only' : 'All sessions'}
240 onPress={async () => {
241 await update($, showAll, v => !v)
242 await refresh($)
243 }}
244 />
245 </Box>
246 )
247
248 if (at === 0) {
249 return (
250 <Box flexDirection="column">
251 <Text dimColor>Reading sessions…</Text>
252 </Box>
253 )
254 }
255
256 const summary = summaryOf(list, known, all ? 'session' : 'worker') + (rest > 0 && !isNarrow ? ` · ${rest} other` : '')
257 const updated = `updated ${age(now - at)} ago`
258
259 return (
260 <Box flexDirection="column" width={width}>
261 {isNarrow ? (
262 <Box flexDirection="column">
263 <Text bold wrap="truncate-end">{summary}</Text>
264 <Text dimColor>{updated}</Text>
265 </Box>
266 ) : (
267 <Box flexDirection="row" justifyContent="space-between" width={width}>
268 <Text bold wrap="truncate-end">{cut(summary, width - updated.length - 2)}</Text>
269 <Text dimColor>{updated}</Text>
270 </Box>
271 )}
272 {failed !== null && (
273 <Text color="error" wrap="truncate-end">
274 ✕ {cut(failed, width - 2)}
275 </Text>
276 )}
277 {list.length === 0 && (
278 <Box flexDirection="column" marginTop={1}>
279 <Text dimColor>No workers yet</Text>
280 <Text dimColor>Start some with /orchestrate</Text>
281 </Box>
282 )}
283 {list.map(w => {
284 const c = contactFor(known, w)
285 const look = lookOf(w, c)
286 const branch = gitOf(w)
287 const right = isNarrow ? '' : branch
288 const nameRoom = width - 2 - (right ? right.length + 2 : 0)
289 const second = isNarrow && branch ? `${branch} · ` : ''
290 const last = lastOf(w, c, now, width - 2 - second.length)
291 return (
292 <Box key={`w-${w.name}`} flexDirection="column" marginTop={1}>
293 <Box flexDirection="row" justifyContent="space-between" width={width}>
294 <Box flexDirection="row">
295 <Text color={look.color} dimColor={look.dim}>
296 {look.glyph}{' '}
297 </Text>
298 <Button
299 key={`tell-${w.name}`}
300 plain
301 label={cut(w.name, Math.max(4, nameRoom))}
302 dimColor={look.rank === 3}
303 onPress={() => void $.prompt.fill({ text: `Tell ${w.name}: ` })}
304 />
305 </Box>
306 {right !== '' && <Text dimColor>{right}</Text>}
307 </Box>
308 <Text dimColor wrap="truncate-end">
309 {' '}
310 {second}
311 {last}
312 </Text>
313 </Box>
314 )
315 })}
316 {footer}
317 </Box>
318 )
319 })
320}
321hooks/lib.ts 142 lines1import type { Contact, Worker } from '../types'
2
3/** One entry of `claude agents --json`. */
4export type AgentEntry = {
5 pid: number
6 cwd: string
7 kind?: string
8 sessionId: string
9 name: string
10 status: string
11 waitingFor?: string
12}
13
14export const parseAgents = (stdout: string): AgentEntry[] => {
15 try {
16 const list: unknown = JSON.parse(stdout)
17 if (!Array.isArray(list)) return []
18 return list.filter(
19 (a): a is AgentEntry =>
20 typeof a === 'object' && a !== null && typeof a.sessionId === 'string' && typeof a.name === 'string',
21 )
22 } catch {
23 return []
24 }
25}
26
27/** `uds:/tmp/cc-socks/16932.sock` -> `pid:16932`; a plain name stays itself. */
28export const contactKey = (address: string): string => {
29 const sock = /cc-socks\/(\d+)\.sock/.exec(address)
30 return sock ? `pid:${sock[1]}` : address.trim()
31}
32
33/**
34 * Who sent a cross-session message, read off its envelope:
35 * `<cross-session-message from="uds:..." from-name="brf-app-reading" ...>`.
36 */
37export const senderOf = (text: string): string | null => {
38 const name = /from-name="([^"]+)"/.exec(text)
39 if (name?.[1]) return name[1]
40 const from = /from="([^"]+)"/.exec(text)?.[1]
41 return from && from !== '...' ? contactKey(from) : null
42}
43
44/** The message body without the envelope tags, on one line. */
45export const bodyOf = (text: string): string =>
46 text
47 .replace(/<\/?cross-session-message[^>]*>/g, ' ')
48 .replace(/\s+/g, ' ')
49 .trim()
50
51export const asksForYou = (body: string): boolean =>
52 /\b(approve|approval|decision|decide|blocked|waiting for)\b/i.test(body) || /\?\s*$/.test(body)
53
54export const contactFor = (
55 contacts: Record<string, Contact>,
56 agent: { name: string; pid: number },
57): Contact | undefined => {
58 const byName = contacts[agent.name]
59 const byPid = contacts[`pid:${agent.pid}`]
60 if (byName && byPid) return byName.at >= byPid.at ? byName : byPid
61 return byName ?? byPid
62}
63
64export const stateOf = (status: string): string =>
65 status === 'busy' ? 'running' : status === 'waiting' ? 'blocked' : status
66
67/**
68 * A worker: a session this one has messaged or heard from, or one running
69 * in a worktree of this session's repository.
70 */
71export const isWorker = (
72 agent: AgentEntry,
73 contacts: Record<string, Contact>,
74 repoRoot: string | null,
75): boolean =>
76 contactFor(contacts, agent) !== undefined ||
77 (repoRoot !== null && agent.cwd.startsWith(`${repoRoot}/.claude/worktrees/`))
78
79export const age = (ms: number): string => {
80 const s = Math.max(0, Math.round(ms / 1000))
81 if (s < 60) return `${s}s`
82 if (s < 3600) return `${Math.floor(s / 60)}m`
83 if (s < 86400) return `${Math.floor(s / 3600)}h`
84 return `${Math.floor(s / 86400)}d`
85}
86
87/** Cuts to `width` cells, ending in … when cut. */
88export const cut = (text: string, width: number): string =>
89 width <= 0 ? '' : text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1)).trimEnd()}…`
90
91/** The one glyph and theme color a worker shows, by what it needs. */
92export type Look = { glyph: string; color?: string; dim: boolean; rank: number; label: string }
93
94export const lookOf = (w: Worker, c: Contact | undefined): Look => {
95 if (w.state === 'gone') return { glyph: '✕', color: 'error', dim: false, rank: 3, label: 'gone' }
96 if (w.state === 'blocked' && w.waitingFor && /limit/i.test(w.waitingFor))
97 return { glyph: '⏸', dim: false, rank: 1, label: 'paused by limit' }
98 if (c?.needsYou || w.state === 'blocked')
99 return { glyph: '▲', color: 'warning', dim: false, rank: 0, label: 'needs you' }
100 if (w.state === 'running') return { glyph: '●', color: 'success', dim: false, rank: 1, label: 'running' }
101 return { glyph: '○', dim: true, rank: 2, label: w.state }
102}
103
104export const sortWorkers = (list: Worker[], contacts: Record<string, Contact>): Worker[] =>
105 [...list].sort((a, b) => {
106 const ra = lookOf(a, contactFor(contacts, a)).rank
107 const rb = lookOf(b, contactFor(contacts, b)).rank
108 if (ra !== rb) return ra - rb
109 const ta = contactFor(contacts, a)?.at ?? 0
110 const tb = contactFor(contacts, b)?.at ?? 0
111 return tb - ta || a.name.localeCompare(b.name)
112 })
113
114/** "4 workers · 2 running · 1 needs you", zero parts left out. */
115export const summaryOf = (list: Worker[], contacts: Record<string, Contact>, noun = 'worker'): string => {
116 const looks = list.map(w => lookOf(w, contactFor(contacts, w)))
117 const count = (rank: number) => looks.filter(l => l.rank === rank && l.label !== 'paused by limit').length
118 const parts = [`${list.length} ${noun}${list.length === 1 ? '' : 's'}`]
119 const running = looks.filter(l => l.label === 'running').length
120 const needs = count(0)
121 const paused = looks.filter(l => l.label === 'paused by limit').length
122 const gone = count(3)
123 if (running) parts.push(`${running} running`)
124 if (needs) parts.push(`${needs} need${needs === 1 ? 's' : ''} you`)
125 if (paused) parts.push(`${paused} paused`)
126 if (gone) parts.push(`${gone} gone`)
127 return parts.join(' · ')
128}
129
130/** "worktree-ui +3 ~2": ahead and dirty counts, zeros left out. */
131export const gitOf = (w: Worker): string =>
132 w.branch === null ? '' : [w.branch, w.ahead ? `+${w.ahead}` : '', w.dirty ? `~${w.dirty}` : ''].filter(Boolean).join(' ')
133
134/** The second line: the last message with its age, or what the session waits for. */
135export const lastOf = (w: Worker, c: Contact | undefined, now: number, width: number): string => {
136 const waiting = w.state === 'blocked' && w.waitingFor ? `waiting: ${w.waitingFor}` : ''
137 if (!c) return cut(waiting || 'No messages yet', width)
138 const when = ` · ${age(now - c.at)}`
139 const body = `${c.dir === 'in' ? '' : 'you: '}${c.text}`
140 return cut(body, width - when.length) + when
141}
142types/index.d.ts 44 lines1export type Contact = {
2 /** Last message text, first 80 characters, one line. */
3 text: string
4 /** When it was sent or received, ms since the epoch. */
5 at: number
6 /** `in` received from the worker, `out` sent to it. */
7 dir: 'in' | 'out'
8 /** The worker asked for a decision and this session has not answered yet. */
9 needsYou: boolean
10}
11
12export type Worker = {
13 name: string
14 pid: number
15 sessionId: string
16 cwd: string
17 /** running, idle or blocked, from `claude agents`; gone once it left that list. */
18 state: string
19 /** What a blocked session waits for, as `claude agents` says it. */
20 waitingFor: string | null
21 branch: string | null
22 ahead: number | null
23 dirty: number | null
24}
25
26declare module 'claude-code' {
27 interface PluginState {
28 workers: {
29 /** Last message per worker, keyed by name, or `pid:<n>` when only the socket is known. */
30 contacts: Record<string, Contact>
31 rows: Worker[]
32 /** Other sessions running that are not counted as workers. */
33 others: number
34 updatedAt: number
35 showAll: boolean
36 error: string | null
37 /** Whether this plugin's pane is open, kept across reloads. */
38 isOpen: boolean
39 /** Workers waiting on the person; the hint line's tail. */
40 needs: number
41 }
42 }
43}
44