SLOPSHOPPER

wod-band

A pixel-art Ragtag athlete above the prompt who does a rep every turn: the session is an AMRAP of thrusters, burpees and pull-ups.

newbandguardcommandtoasttimer
★ 1v0.3.1MITupdated 2026-09-21yash-gadodia/claude-mods/wod-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · wod-band
› fix the failing auth test and add an audit log call ╭──────────────╮ │ wod-band │ ⏺ Read(src/auth.ts) │ round 1 done │ ⎿ Read 6 lines ╰──────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /wod ⎿ wod-band: wod band on. every tool call is a rep. ▀▄ ▄▄ ▄▀ AMRAP 0:00 ▀▀▀▀▀▀ round 2 · burpees 3/5 · no rep! ▄▀ ▄▀▀▄▀▄ 8 reps · 3 thrusters · 3 burpees · 2 pull-ups · 8 all-time ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
▀▄ ▄▄ ▄▀ AMRAP 0:00 ▀▀▀▀▀▀ round 2 · burpees 3/5 · no rep! ▄▀ ▄▀▀▄▀▄ 8 reps · 3 thrusters · 3 burpees · 2 pull-ups · 8 all-time ⟨Claude Code's own drawing⟩
README

claude-mods

Mods that keep an agent honest — plus a few that make the terminal fun.

claude-code · mod · function-hooks · typescript · macos

License: MIT Claude Code — mod test

usage  max  volty  opus-5  5h 34%  7d 12%  ctx 41% 82k  $1.23
scope: 3/4 files
 ▸

<sub>Thirteen mods, each drawing or guarding its own slice of the session. Above: usage-band and scope-guard.</sub>

usage-band, wod-band and wod-timer above the prompt <sub>usage-band, wod-band and wod-timer in a live session.</sub>

Why

Claude Code will tell you a deploy worked because git push exited 0. It will turn a one-line fix into a nine-file refactor and never mention it. Written rules in CLAUDE.md help until the model forgets them, and you find out on the deploy that breaks.

These are the same rules, moved out of prose and into the engine — where they hold whether or not the model remembers.

What a mod is

A mod is a Claude Code plugin whose behaviour lives in a TypeScript hooks module — register(on, options) wiring handlers onto engine events (tool.call, ui.render, turn.complete) rather than markdown the model reads. A mod can deny a tool call, rewrite it in flight, draw above the prompt, or put evidence in front of the model that it cannot argue with.

Every mod here is source you can read in one sitting. None of them phone home: there is no $.http.fetch anywhere in this repo.

Install

Function hooks are behind a flag. Set it first, in your shell profile or settings.json env:

export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1

Then, in Claude Code:

/plugin marketplace add yash-gadodia/claude-mods
/plugin install scope-guard@claude-mods

Install only what you want — each mod is independent. Update with claude plugin update <name>@claude-mods.

The mods

Discipline

ModWhat it does
scope-guardCounts the distinct files one turn edits. At the threshold it stops and makes the goal get restated, so a small ask cannot quietly become a refactor. /scope sets it.
deploy-verifyAfter a deploy command succeeds, waits for the GitHub Actions run it started, then curls the live URL with cache-busting and puts the verdict in the model's context. A deploy cannot be claimed without evidence.
receiptThe turn footer becomes a receipt: edits, runs and curls, with a warning when edits ran nothing. Destructive commands are never folded into a tool group, a claim of "fixed" with no run puts "unverified claim pending" in the spinner, and Tab suggests running the tests.
diff-reviewA docked pane with each edited file's hunk and keep or revert buttons. Reverting runs git directly; no model turn.
merge-gateDenies gh pr merge, a git merge on trunk, or a push to main unless the latest human message contains the word merge. Ship, push and deploy do not count. /merge-gate toggles it.
mini-offloadRewrites heavy Bash commands (test suites, builds, Docker) to run on a second machine over ssh — syncing the commit there first, because the remote checkout is the real hazard. /mini sets always, ask, or off.

Instruments

ModWhat it does
usage-bandThe 5-hour and 7-day limit windows, this session's context fill and cost, above the prompt. Nudges you to /clear when the window gets expensive.
money-bandLiquid assets, CPF, debt and month-to-date spend, read from a pair of SQLite databases over ssh. Every figure is the database's own; nothing is estimated.
copy-bandClick-to-copy buttons above the prompt for every code block and quoted draft in the last answer, plus a durable stash of older ones. Copying runs pbcopy directly — no model turn.
done-blinkWhen a turn lands, the iTerm2 tab blinks orange every half second until you send the next prompt or three minutes pass, so a finished session is obvious from any other tab. Works inside tmux with no passthrough config: the escape goes to the tmux client's tty. /done-blink 60 sets the ceiling.
chrome-switchSwitches the Claude in Chrome extension between named browser profiles using select_browser, which needs no approval click. /chromep maps them.

Fun

ModWhat it does
wod-bandA pixel-art athlete above the prompt who does a rep every turn. The session is an AMRAP of thrusters, burpees and pull-ups.
wod-timer3, 2, 1, GO in the spinner when you submit, a running gym clock while Claude works, and a whiteboard split in the footer when the turn lands: turn, time, AMRAP total, PR. /wod-timer voice on reads long splits aloud.

Turning them off

Every mod checks one environment variable before doing anything:

CLAUDE_MODS_DISABLE=all            # every mod in this repo becomes a pass-through
CLAUDE_MODS_DISABLE=scope-guard    # just that one
CLAUDE_MODS_DISABLE=wod-band,wod-timer

A disabled mod registers no command and every hook falls straight through to next(e).

Configuration

Mods that touch your machine declare their settings in plugin.json userConfig, so they are editable through /config rather than by hand:

  • scope-guard — /scope <n> sets the file threshold. /scope judge on|off (default on) lets a one-shot Haiku call decide at the threshold whether the next edit is still inside the goal you stated first; a yes raises the ceiling by one for that turn, a no or a failed call falls back to asking. /scope off disables the guard.
  • merge-gate — /merge-gate on|off. "merge x3" or "merge after each" in your message grants that many merges.
  • mini-offload — host (ssh alias, default mini), remotePath (the PATH export prefixed to every offloaded command). Per-repo overrides live at <repo>/.claude/mini-offload.json.
  • money-band — host, networthDb, financeDb. Expects SQLite databases with accounts/balances and transactions tables. efAccount (default UOB One) and efTarget (default 30000) feed the EF 41% footer label.
  • usage-band — sgdRate (default 1.30) for the S$ footer label; /usage-band sgd off hides it.
  • receipt — /receipt on|off|status.
  • done-blink — /done-blink on|off|status|<seconds> (default 180, max 900).
  • diff-review — /diff-review open|close|on|off.
  • deploy-verify — per-repo, at <repo>/.claude/deploy-verify.json: ``json { "url": "https://example.com", "matchFile": "VERSION" } ``
  • chrome-switch — ~/.claude/chrome-browsers.json, mapping labels to deviceIds.

Evidence, not decoration

scope-guard and deploy-verify also write a block into the model's own context (prompt.context), replacing their previous copy rather than accumulating:

# deployVerify
Last live deploy check, 2 minutes ago:
  VERIFIED live: https://example.com served "v3.10.10"
This is the only evidence about the live site in this session. Do not describe the deploy as
verified unless a line above starts with VERIFIED, and do not re-state an older claim over it.

A band above the prompt is for you. A context block is for the model — and it cannot be talked around. Repeated advisories are hashed and suppressed for a cooldown so this costs context once, not once per tool call; verdicts themselves are never throttled, because a verdict is evidence.

Tests

npm install
npm test

npm test typechecks every mod, runs its suite under claude plugin test (the official kit, claude-code/testing, with a mocked clock, store and process table), and checks each mod's footprint: the hooks, $ calls and env reads that claude plugin validate reports, pinned in <mod>/FOOTPRINT. A mod that starts calling $.http.fetch fails the build instead of a README sentence going stale. scripts/footprint.sh --write re-pins after a deliberate change.

The interesting half of deploy-verify's suite is the clean baseline: commands that mention a deploy without being one — echo "git push", grep -r "wrangler deploy", git push --dry-run, a commit message quoting make deploy, a heredoc containing one. A false positive curls a live URL nothing was pushed to and then reports a verdict about it, which is worse than not checking at all.

Design rules

The ones that survived contact with real sessions:

  1. A deny always carries the fix. Blocking without saying what to do instead strands the model in a retry loop. Every refusal here names the next action.
  2. Never block when there is no way through. If the only outcomes are "denied" and "denied again", let it run and say something instead.
  3. A render hook that throws takes the whole mod down with it. Every band wraps its frame in try/catch and falls back to what was there.
  4. Hooks have ten seconds of their own time. next(e) and $ calls are free; $.clock.sleep is not. Past the budget, or on a throw, the engine skips the hook silently unless it declares .catch — so every guard here catches and denies, and slow work belongs on a timer.
  5. Bands yield. e.props.hasSurvey means the engine wants that slot; give it back.

Requirements

Claude Code 2.1.271+ with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. macOS — copy-band shells out to pbcopy, and mini-offload/money-band assume ssh and a Homebrew path on the remote.

License

MIT

Source 1 files
hooks/register.tsx 454 lines
1/* @jsx h */
2import type { EngineInterface, Register } from 'claude-code'
3
4// A tiny Ragtag athlete above the prompt. Every completed tool call is a rep of the movement that
5// tool is: an edit is a thruster, a shell command a burpee, a read or search a pull-up. Five reps
6// make a round and the clock is the session's AMRAP. The sprite is a 13x6 pixel grid drawn with
7// half-blocks (one cell carries two pixels, top as the glyph's colour and bottom as its background)
8// into one Raster, three rows tall. Frames are encoded once at load and repainted in place with
9// $.ui.blit; the caption is only redrawn when its text changes.
10
11const REPS_PER_ROUND = 5
12const FRAME_MS = 280
13const CHALK_TICKS = 5
14const NOREP_TICKS = 6
15const REST_TICKS = 150
16const SHOWN_KEY = 'wod-band:shown'
17const LIFETIME_KEY = 'wod-band:lifetime-reps'
18const RASTER_KEY = 'wod'
19const COLUMNS = 13
20const ROWS = 3
21const GAP = 2
22const NARROW = 45
23const WIDE = 60
24const DEFAULT = 0x01000000
25
26const PALETTE: Record<string, number> = {
27  h: 0xe8b98a, // head / skin
28  s: 0xe8b98a, // limbs
29  r: 0xdd3b2a, // ragtag red singlet
30  k: 0x2b2b2b, // shorts
31  b: 0xb8b8b8, // bar
32  p: 0xdd3b2a, // plates
33  w: 0xf4f1ea, // chalk
34  x: 0xe05252, // no rep
35}
36
37// prettier-ignore
38const THRUSTER = [
39  ['......hh.....',
40   '.pbbbbbbbbbp.',
41   '.....srrs....',
42   '......rr.....',
43   '......kk.....',
44   '.....s..s....'],
45  ['.............',
46   '......hh.....',
47   '.pbbbbbbbbbp.',
48   '.....srrs....',
49   '....sskkss...',
50   '.............'],
51  ['......hh.....',
52   '.pbbbbbbbbbp.',
53   '.....srrs....',
54   '......rr.....',
55   '......kk.....',
56   '.....s..s....'],
57  ['.pbbbbbbbbbp.',
58   '.....s..s....',
59   '......hh.....',
60   '.....srrs....',
61   '......kk.....',
62   '.....s..s....'],
63]
64// prettier-ignore
65const BURPEE = [
66  ['......hh.....',
67   '.....srrs....',
68   '......rr.....',
69   '......kk.....',
70   '.....s..s....',
71   '.............'],
72  ['.............',
73   '.............',
74   '.............',
75   '.hh..........',
76   '.srrrrkkss...',
77   '.s.......s...'],
78  ['.............',
79   '......hh.....',
80   '.....srrs....',
81   '....sskkss...',
82   '.............',
83   '.............'],
84  ['.....s..s....',
85   '......hh.....',
86   '.....srrs....',
87   '......kk.....',
88   '.....s..s....',
89   '.............'],
90]
91// prettier-ignore
92const PULLUP = [
93  ['.bbbbbbbbbbb.',
94   '.....s..s....',
95   '.....s..s....',
96   '......hh.....',
97   '.....srrs....',
98   '......kk.....'],
99  ['.bbbbbbbbbbb.',
100   '.....s..s....',
101   '......hh.....',
102   '.....srrs....',
103   '......kk.....',
104   '.............'],
105  ['......hh.....',
106   '.bbbsrrsbbb..',
107   '......rr.....',
108   '......kk.....',
109   '.....s..s....',
110   '.............'],
111  ['.bbbbbbbbbbb.',
112   '.....s..s....',
113   '......hh.....',
114   '.....srrs....',
115   '......kk.....',
116   '.............'],
117]
118// prettier-ignore
119const REST = [
120  ['......hh.....',
121   '.....srrs....',
122   '......rr.....',
123   '......kk.....',
124   '.....s..s....',
125   '.pbbbbbbbbbp.'],
126]
127// prettier-ignore
128const CHALK = [
129  ['......hh.....',
130   '....wsrrsw...',
131   '......rr.....',
132   '......kk.....',
133   '.....s..s....',
134   '.pbbbbbbbbbp.'],
135]
136// prettier-ignore
137const NOREP = [
138  ['.x.........x.',
139   '..x..hh...x..',
140   '...xsrrsx....',
141   '....x.rrx....',
142   '...x..kk.x...',
143   '..x..s..s.x..'],
144]
145
146const HALF_TOP = 0x2580
147const HALF_BOTTOM = 0x2584
148const SPACE = 0x20
149
150export function cellsFor(grid: readonly string[]): string {
151  const words = new Uint32Array(COLUMNS * ROWS * 3)
152  for (let y = 0; y < ROWS; y++) {
153    const top = grid[y * 2] ?? ''
154    const bot = grid[y * 2 + 1] ?? ''
155    for (let x = 0; x < COLUMNS; x++) {
156      const i = (y * COLUMNS + x) * 3
157      const tc = PALETTE[top[x] ?? '.']
158      const bc = PALETTE[bot[x] ?? '.']
159      if (tc !== undefined) {
160        words[i] = HALF_TOP
161        words[i + 1] = tc
162        words[i + 2] = bc ?? DEFAULT
163      } else if (bc !== undefined) {
164        words[i] = HALF_BOTTOM
165        words[i + 1] = bc
166        words[i + 2] = DEFAULT
167      } else {
168        words[i] = SPACE
169        words[i + 1] = DEFAULT
170        words[i + 2] = DEFAULT
171      }
172    }
173  }
174  const bytes = new Uint8Array(words.buffer) as unknown as { toBase64: () => string }
175  return bytes.toBase64()
176}
177
178type Movement = 'thrusters' | 'burpees' | 'pull-ups'
179const MOVEMENTS: Movement[] = ['thrusters', 'burpees', 'pull-ups']
180const FRAMES: Record<Movement, string[]> = {
181  thrusters: THRUSTER.map(cellsFor),
182  burpees: BURPEE.map(cellsFor),
183  'pull-ups': PULLUP.map(cellsFor),
184}
185export const REST_CELLS = cellsFor(REST[0]!)
186export const CHALK_CELLS = cellsFor(CHALK[0]!)
187export const NOREP_CELLS = cellsFor(NOREP[0]!)
188
189const TOOL_MOVEMENT: Record<string, Movement> = {
190  Edit: 'thrusters',
191  Write: 'thrusters',
192  MultiEdit: 'thrusters',
193  NotebookEdit: 'thrusters',
194  Bash: 'burpees',
195  Read: 'pull-ups',
196  Grep: 'pull-ups',
197  Glob: 'pull-ups',
198  Agent: 'pull-ups',
199}
200
201let shown = true
202let reps = 0
203let byMovement: Record<Movement, number> = { thrusters: 0, burpees: 0, 'pull-ups': 0 }
204let lifetime = 0
205let startedAt = 0
206let frame = 0
207let running = false
208let movement: Movement = 'thrusters'
209let chalkTicks = 0
210let norepTicks = 0
211let idleTicks = 0
212let requestId: string | undefined
213let painted = ''
214let caption = ''
215let tick: { cancel: () => void } | undefined
216
217const round = () => Math.floor(reps / REPS_PER_ROUND)
218
219const clock = (ms: number) => {
220  const s = Math.max(0, Math.floor(ms / 1000))
221  const h = Math.floor(s / 3600)
222  const m = Math.floor((s % 3600) / 60)
223  const sec = s % 60
224  const mm = h > 0 ? String(m).padStart(2, '0') : String(m)
225  return `${h > 0 ? `${h}:` : ''}${mm}:${String(sec).padStart(2, '0')}`
226}
227
228const currentCells = () => {
229  if (norepTicks > 0) return NOREP_CELLS
230  if (running) {
231    const frames = FRAMES[movement]
232    return frames[frame % frames.length]!
233  }
234  if (chalkTicks > 0) return CHALK_CELLS
235  if (idleTicks >= REST_TICKS) return REST_CELLS
236  return FRAMES[movement][0]!
237}
238
239const captionText = (now: number) =>
240  [
241    running ? clock(now - startedAt) : '',
242    round(),
243    movement,
244    reps % REPS_PER_ROUND,
245    running,
246    chalkTicks > 0,
247    norepTicks > 0,
248    reps,
249    lifetime,
250  ].join('|')
251
252async function paint($: EngineInterface) {
253  if (!requestId) return
254  const cells = currentCells()
255  if (cells === painted) return
256  painted = cells
257  const r = await $.ui.blit({ requestId, key: RASTER_KEY, cells }).catch(() => ({ deny: 'threw' }))
258  if (r.deny) requestId = undefined
259}
260
261async function redraw($: EngineInterface) {
262  const now = await $.clock.now()
263  const text = captionText(now)
264  if (text === caption) return
265  caption = text
266  $.ui.invalidate('ui.render')
267}
268
269async function onTick($: EngineInterface) {
270  if (norepTicks > 0) norepTicks -= 1
271  else if (running) frame += 1
272  else {
273    if (chalkTicks > 0) chalkTicks -= 1
274    idleTicks += 1
275  }
276  await paint($)
277  await redraw($)
278}
279
280async function rep($: EngineInterface) {
281  reps += 1
282  lifetime += 1
283  byMovement[movement] += 1
284  chalkTicks = CHALK_TICKS
285  await $.store.set(LIFETIME_KEY, lifetime).catch(() => undefined)
286  if (reps % REPS_PER_ROUND === 0) $.ui.toast(`round ${round()} done`, { timeoutMs: 4000 })
287  await redraw($)
288}
289
290// The first thing anyone does when a mod misbehaves is try to turn it off. `CLAUDE_MODS_DISABLE=all`,
291// or a comma list naming this mod, makes every hook here a pass-through and registers no command.
292const MOD = 'wod-band'
293let disabled = false
294const readDisabled = async ($: EngineInterface): Promise<boolean> => {
295  const raw = (await $.env.get('CLAUDE_MODS_DISABLE').catch(() => undefined)) ?? ''
296  disabled = raw
297    .split(',')
298    .map(v => v.trim())
299    .some(v => v === 'all' || v === MOD)
300  return disabled
301}
302
303export const register: Register = (on) => {
304  on('session.start', async ($, e, next) => {
305    const r = await next(e)
306    if (await readDisabled($)) return r
307    if ((await $.store.get(SHOWN_KEY).catch(() => undefined)) === false) shown = false
308    const stored = await $.store.get(LIFETIME_KEY).catch(() => undefined)
309    if (typeof stored === 'number') lifetime = stored
310    startedAt = await $.clock.now()
311    tick?.cancel()
312    tick = $.clock.every(FRAME_MS, () => void onTick($))
313    await $.command
314      .register({
315        name: 'wod',
316        description: 'The pixel athlete above the prompt: reps per tool call, AMRAP clock (wod-band)',
317        argumentHint: '[on | off | reset]',
318        immediate: true,
319      })
320      .catch((err) => $.ui.log(`wod-band: /wod not registered: ${err}`))
321    return r
322  })
323
324  on('command.run', { command: 'wod' }, async ($, e) => {
325    const arg = e.args.trim().toLowerCase()
326    if (arg === 'off') {
327      shown = false
328      await $.store.set(SHOWN_KEY, false).catch(() => undefined)
329      $.ui.invalidate('ui.render')
330      return { text: 'wod band off. rest day.' }
331    }
332    if (arg === 'reset') {
333      reps = 0
334      byMovement = { thrusters: 0, burpees: 0, 'pull-ups': 0 }
335      startedAt = await $.clock.now()
336      $.ui.invalidate('ui.render')
337      return { text: '3, 2, 1... clock reset, new AMRAP' }
338    }
339    if (arg === 'on' || arg === '') {
340      shown = true
341      await $.store.set(SHOWN_KEY, true).catch(() => undefined)
342      $.ui.invalidate('ui.render')
343      return { text: 'wod band on. every tool call is a rep.' }
344    }
345    return { text: `wod: no such argument "${arg}" — use on, off or reset` }
346  })
347
348  on('turn.start', async ($, e, next) => {
349    if (disabled) return next(e)
350    running = true
351    frame = 0
352    idleTicks = 0
353    await paint($)
354    await redraw($)
355    return next(e)
356  })
357
358  on('tool.call', { tool: /^(Edit|Write|MultiEdit|NotebookEdit|Bash|Read|Grep|Glob|Agent)$/ }, async ($, e, next) => {
359    if (disabled) return next(e)
360    const next_ = TOOL_MOVEMENT[e.tool]!
361    if (next_ !== movement) {
362      movement = next_
363      frame = 0
364    }
365    running = true
366    idleTicks = 0
367    await paint($)
368    const r = await next(e)
369    if (r.isError === true) {
370      norepTicks = NOREP_TICKS
371      await paint($)
372      await redraw($)
373    } else if (r.deny === undefined) await rep($)
374    return r
375  })
376
377  on('turn.complete', async ($, e, next) => {
378    if (disabled) return next(e)
379    const r = await next(e)
380    running = false
381    idleTicks = 0
382    await paint($)
383    await redraw($)
384    return r
385  })
386
387  on('turn.abort', async ($, e, next) => {
388    if (disabled) return next(e)
389    const r = await next(e)
390    running = false
391    idleTicks = 0
392    await paint($)
393    await redraw($)
394    return r
395  })
396
397  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
398    if (disabled) return next(e)
399    if (!shown || e.props.hasSurvey || e.surface !== 'terminal') return next(e)
400    try {
401      const { Box, Text, Raster } = await $.ui.resolve(e)
402      const rest = await next(e)
403      requestId = e.requestId
404      painted = currentCells()
405      const now = await $.clock.now()
406      caption = captionText(now)
407      const inRound = reps % REPS_PER_ROUND
408      const width = e.props.bodyColumns ?? 80
409      const status = norepTicks > 0 ? ' · no rep!' : chalkTicks > 0 ? ' · rep!' : running ? ' · working' : ''
410      const statusColor = norepTicks > 0 ? 'red' : chalkTicks > 0 ? 'green' : 'yellow'
411      if (width < NARROW) {
412        requestId = undefined
413        const head = `AMRAP ${clock(now - startedAt)} · r${round() + 1} · ${movement} ${inRound}/${REPS_PER_ROUND}`
414        return (
415          <Box flexDirection="column">
416            <Text wrap="truncate-end">
417              <Text bold color="#dd3b2a">{head.slice(0, width)}</Text>
418              <Text bold color={statusColor}>{head.length + status.length <= width ? status : ''}</Text>
419            </Text>
420            {rest}
421          </Box>
422        )
423      }
424      const cap = width - COLUMNS - GAP
425      const roundHead = `round ${round() + 1} · ${movement} ${inRound}/${REPS_PER_ROUND}`
426      const tally = width < WIDE
427        ? `${reps} reps`
428        : `${reps} reps · ${MOVEMENTS.map((m) => `${byMovement[m]} ${m}`).join(' · ')} · ${lifetime} all-time`
429      return (
430        <Box flexDirection="column">
431          <Box flexDirection="row" columnGap={GAP}>
432            <Raster key={RASTER_KEY} columns={COLUMNS} rows={ROWS} cells={painted} />
433            <Box flexDirection="column">
434              <Text wrap="truncate-end">
435                <Text bold color="#dd3b2a">AMRAP</Text>
436                <Text dimColor>{` ${clock(now - startedAt)}`.slice(0, Math.max(0, cap - 5))}</Text>
437              </Text>
438              <Text key="round" wrap="truncate-end">
439                <Text dimColor>{roundHead.slice(0, cap)}</Text>
440                <Text bold color={statusColor}>{roundHead.length + status.length <= cap ? status : ''}</Text>
441              </Text>
442              <Text key="tally" dimColor wrap="truncate-end">{tally.slice(0, cap)}</Text>
443            </Box>
444          </Box>
445          {rest}
446        </Box>
447      )
448    } catch (err) {
449      $.ui.log(`wod-band: render failed: ${err}`)
450      return next(e)
451    }
452  })
453}
454