SLOPSHOPPER

judy-board

Judy the Judge's Live Board in a pane, a status line and toasts when teams file or verdicts land (Gavel & Gown, Organized AI).

newpanecommandtoaststatusnetwork
v0.1.0no licenseupdated 2026-10-19Organized-AI/judy-mods/judy-board
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · judy-board
│ ┃ Judy's Live Board ✕ › fix the failing auth test and add an audit log call │ ┃ Gavel & Gown · Live Board │ ┃ ⚖ Judy unreachable ⏺ Read(src/auth.ts) │ ┃ Error: Judy answered HTTP 0 ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ No teams yet. ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ [ Refresh ] ⎿ 3 pass, 1 fail │ ┃ https://judy-the-judge.jordan-691.workers.de │ ┃ v/hq ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /judy-board │ ⎿ judy-board: Live Board opened. On the web: https://judy-the-judg │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ judy-board: ⚖ Judy unreachable

Draws

Pane · Judy's Live Board
Gavel & Gown · Live Board ⚖ Judy unreachable Error: Judy answered HTTP 0 No teams yet. [ Refresh ] https://judy-the-judge.jordan-691.workers.dev/hq
README

judy-mods

Claude Code mods for Judy the Judge, the LLM judge behind Gavel & Gown (Organized AI).

Live Board: https://judy-the-judge.jordan-691.workers.dev/hq

ModWhat it does
judy-boardJudy's Live Board in a pane beside the chat. It adds a status line with team count, nightly eval gates and the MLflow range, and toasts when a team files, a trial ruling lands or an audit finishes or fails.
judy-guardJudy's project rules, checked on every tool call instead of remembered: the court code and Keychain secrets stay out of files, commits and the transcript; Judy deploys through its gate; Jev is called only through jev-gateway.

Install

At the prompt of a Claude Code terminal session:

/plugin install judy-board --marketplace Organized-AI/judy-mods
/plugin install judy-guard --marketplace Organized-AI/judy-mods

Answer y to add the marketplace, then pick a scope (user scope is first; Enter takes it). Each mod is active straight away.

From a shell:

claude plugin install judy-board --marketplace Organized-AI/judy-mods
claude plugin install judy-guard --marketplace Organized-AI/judy-mods

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with Organized-AI/judy-mods, then Add plugin.

If this repository is private, it installs only for people who can read it on GitHub and whose machine already holds a git credential for github.com (an SSH key, or gh auth login then gh auth setup-git). The install never prompts for one.

judy-board

  • /judy-board opens the Live Board pane: teams ranked by trial score, then practice score, with each team's ruling, practice standing and any audit error, plus a Refresh button.
  • /judy-team owner/repo (or a GitHub URL) puts one team's card in the chat: trial ruling and score, practice score and standing with the change since the last audit, and the link to its team card.
  • The status line reads like ⚖ Judy · 12 teams · 1 running · evals 13/13 · range up and refreshes every minute. On a refresh, up to three toasts announce what changed.

It reads only public endpoints: Judy's /api/hq and the event guide's /judy/api/status. It needs no key or court code.

judy-guard

RuleRefusesAllowed route
1. The court code stays in the chatAny command or file write whose text contains today's court code, in any caseScripts read it from JUDY_EVENT_CODE or the Keychain
2. No secret printed into the transcriptsecurity find-generic-password … -w with its output neither piped, redirected nor captured; cat/head/tail/less of ~/.config/judy/{aig-token,event-code,openrouter-key} or .dev.vars`… -w \wrangler secret put NAME, or "$(security find-generic-password -s NAME -w)"`
3. Judy deploys through its gateA bare wrangler … deploy in judy-the-judge (as the working directory, or after cd …judy-the-judge &&)npm run deploy (typecheck, tests, the eval gate, D1 migrations, then wrangler), or --dry-run
4. Jev only through jev-gatewayapi.typesafe.ai, TYPESAFE_API_KEY, typesafe/jev or a typesafe:/ model in a command outside jev-gateway, or in a code file written outside itPOST /v1/systemone on jev-gateway with JEV_GATEWAY_TOKEN. Docs (.md, .mdx, .txt) may name the rule.

A refused call reaches the model as an error naming the rule, so it can take the allowed route, and a toast shows which rule fired.

The court code. At session start the guard reads it from the JUDY_EVENT_CODE environment variable or, on a Mac, from the Keychain item JUDY_EVENT_CODE (security add-generic-password -s JUDY_EVENT_CODE -a judy -w, which prompts for the value). It keeps the code in memory only, never in a file or $.state. With no code available, rule 1 is off and the other three still apply.

If a check fails, the guard lets the call through and says so on the status line. A broken guard must not stop all work.

Developing

claude --plugin-dir ./judy-board        # run a mod from its folder; edits hot-reload
claude plugin validate ./judy-guard     # manifest, hooks module and what the engine would refuse
claude plugin test ./judy-guard         # 16 cases, one per rule edge
claude plugin test ./judy-board         # status line, pane on every surface, /judy-team

Related

Source 2 files
hooks/register.tsx 178 lines
1// Judy the Judge's Live Board, beside the chat: the standings from /api/hq, refreshed every minute, a status line
2// (teams, last night's eval gates, the MLflow range), and a toast when a team files or a verdict lands.
3// /judy-board opens the pane; /judy-team <owner/repo> puts one team's card in the chat.
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import type { Board, TeamRow } from '../types'
8
9const JUDY = 'https://judy-the-judge.jordan-691.workers.dev'
10const BOARD_URL = `${JUDY}/hq`
11const GUIDE_STATUS = 'https://hack.organizedai.vip/judy/api/status'
12const PANE = 'judy-board'
13const EVERY_MS = 60_000
14const board = atom({ plugin: 'judy-board', key: 'board' } as const, null)
15
16const num = (v: unknown): number | null => (typeof v === 'number' && Number.isFinite(v) ? v : null)
17const fmt = (v: number | null) => (v === null ? '—' : v.toFixed(1))
18
19function rowOf(t: any): TeamRow {
20  const a = t?.audit ?? {}
21  const tr = t?.trial ?? {}
22  const status = String(a.status ?? '')
23  const audit: TeamRow['audit'] =
24    status === 'error' ? 'error' : num(a.score) !== null ? 'done' : a.live || status === 'running' ? 'running' : status ? 'queued' : 'none'
25  return {
26    key: String(t?.key ?? `${t?.owner}/${t?.repo}`),
27    team: String(t?.team ?? t?.repo ?? t?.key ?? '?'),
28    trial: num(tr.score),
29    ruling: tr.ruling ?? null,
30    trialRunning: !!tr.running,
31    practice: num(a.score),
32    standing: a.standing ?? null,
33    audit,
34    auditError: audit === 'error' ? String(a.error ?? 'the audit failed').slice(0, 160) : null,
35    delta: num(a.delta),
36  }
37}
38
39// Ranked as the board's default: trial score, then practice, then name.
40const rank = (rows: TeamRow[]) =>
41  [...rows].sort((x, y) => (y.trial ?? -1) - (x.trial ?? -1) || (y.practice ?? -1) - (x.practice ?? -1) || x.team.localeCompare(y.team))
42
43async function fetchBoard($: EngineInterface, previous: Board | null): Promise<Board> {
44  const now = await $.clock.now()
45  try {
46    const hq = await $.http.fetch(`${JUDY}/api/hq`, { headers: { accept: 'application/json' } })
47    if (!hq.ok) throw new Error(`Judy answered HTTP ${hq.status}`)
48    const d = JSON.parse(hq.text)
49    let evals: string | null = previous?.evals ?? null
50    let range: boolean | null = previous?.range ?? null
51    try {
52      const s = await $.http.fetch(GUIDE_STATUS, { headers: { accept: 'application/json' } })
53      if (s.ok) {
54        const j = JSON.parse(s.text)
55        evals = j?.judy?.live_evals?.gates ?? null
56        range = j?.mlflow?.configured ? !!j.mlflow.up : null
57      }
58    } catch {
59      // The guide's status is a nicety; the board still draws without it.
60    }
61    return {
62      fetchedAt: now,
63      updatedAt: d?.updated_at ?? null,
64      teams: rank((d?.teams ?? []).map(rowOf)),
65      running: { audits: Number(d?.counts?.audits_running ?? 0), trials: Number(d?.counts?.trials_running ?? 0) },
66      evals,
67      range,
68      error: null,
69    }
70  } catch (err) {
71    return { ...(previous ?? { updatedAt: null, teams: [], running: { audits: 0, trials: 0 }, evals: null, range: null }), fetchedAt: now, error: String(err).slice(0, 160) }
72  }
73}
74
75/** What changed between two reads, as toasts: a new team, a trial ruling, a finished or failed audit. */
76function news(before: Board | null, after: Board): string[] {
77  if (!before || before.error) return []
78  const old = new Map(before.teams.map(t => [t.key, t]))
79  const out: string[] = []
80  for (const t of after.teams) {
81    const was = old.get(t.key)
82    if (!was) out.push(`⚖ New on the board: ${t.team}`)
83    else {
84      if (t.ruling && t.ruling !== was.ruling) out.push(`⚖ ${t.team}: trial ${t.ruling} (${fmt(t.trial)})`)
85      if (t.audit === 'done' && was.audit !== 'done') out.push(`⚖ ${t.team}: practice ${fmt(t.practice)}${t.standing ? `, ${t.standing}` : ''}`)
86      if (t.audit === 'error' && was.audit !== 'error') out.push(`⚖ ${t.team}: audit failed`)
87    }
88  }
89  return out
90}
91
92function statusLine(b: Board): string {
93  if (b.error) return '⚖ Judy unreachable'
94  const busy = b.running.audits + b.running.trials
95  return [
96    `⚖ Judy · ${b.teams.length} team${b.teams.length === 1 ? '' : 's'}`,
97    busy ? `${busy} running` : null,
98    b.evals ? `evals ${b.evals}` : null,
99    b.range === null ? null : `range ${b.range ? 'up' : 'down'}`,
100  ].filter(Boolean).join(' · ')
101}
102
103async function refresh($: EngineInterface, toast: boolean) {
104  const before = await read($, board)
105  const after = await fetchBoard($, before)
106  await update($, board, () => after)
107  $.ui.status(statusLine(after))
108  if (toast) for (const line of news(before, after).slice(0, 3)) $.ui.toast(line)
109}
110
111function card(t: TeamRow): string {
112  return [
113    `**${t.team}** (${t.key}) on Judy's Live Board: ${BOARD_URL}`,
114    `- Trial: ${t.trialRunning ? 'running' : t.ruling ? `${t.ruling}, ${fmt(t.trial)}` : 'not yet'}`,
115    `- Practice: ${t.audit === 'done' ? `${fmt(t.practice)}${t.standing ? `, ${t.standing}` : ''}${t.delta ? ` (${t.delta > 0 ? '+' : ''}${t.delta.toFixed(1)} since last audit)` : ''}` : t.audit === 'error' ? `failed: ${t.auditError}` : t.audit}`,
116    `- Team card: ${JUDY}/api/teams/${t.key}`,
117  ].join('\n')
118}
119
120export const register: Register = on => {
121  on('session.start', async ($, e, next) => {
122    await $.command.register({ name: 'judy-board', description: "Open Judy's Live Board in a pane" })
123    await $.command.register({ name: 'judy-team', description: "Show one team's verdicts: /judy-team owner/repo" })
124    await refresh($, false)
125    $.clock.every(EVERY_MS, () => refresh($, true))
126    return next(e)
127  })
128
129  on('command.run', { command: 'judy-board' }, async $ => {
130    await refresh($, false)
131    await $.ui.open({ id: PANE, title: "Judy's Live Board" })
132    return { text: `Live Board opened. On the web: ${BOARD_URL}` }
133  })
134
135  on('command.run', { command: 'judy-team' }, async ($, e) => {
136    const want = e.args.trim().replace(/^https:\/\/github\.com\//, '').replace(/\/+$/, '').toLowerCase()
137    await refresh($, false)
138    const b = await read($, board)
139    if (!b || b.error) return { text: `Judy isn't answering right now${b?.error ? ` (${b.error})` : ''}.` }
140    if (!want) return { text: `Teams on the board: ${b.teams.map(t => t.key).join(', ') || 'none yet'}. Use /judy-team owner/repo.` }
141    const t = b.teams.find(x => x.key.toLowerCase() === want || x.team.toLowerCase() === want)
142    return { text: t ? card(t) : `${want} isn't on the board. Teams: ${b.teams.map(x => x.key).join(', ') || 'none yet'}.` }
143  })
144
145  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
146    const { Box, Text, Button } = $.ui.resolve(e)
147    const b = await read($, board)
148    const room = Math.max(3, (e.viewport?.rows ?? 30) - 8)
149    if (!b) return <Text dimColor>Reading the board…</Text>
150    return (
151      <Box flexDirection="column">
152        <Text bold>Gavel &amp; Gown · Live Board</Text>
153        <Text dimColor>{statusLine(b)}</Text>
154        {b.error && <Text color="red">{b.error}</Text>}
155        <Box flexDirection="column" marginTop={1}>
156          {b.teams.length === 0 && <Text dimColor>No teams yet.</Text>}
157          {b.teams.slice(0, room).map((t, i) => (
158            <Box key={t.key} flexDirection="column">
159              <Text>
160                {String(i + 1).padStart(2, ' ')}. <Text bold>{t.team}</Text>
161              </Text>
162              <Text dimColor>
163                {'    '}trial {t.trialRunning ? 'running' : t.ruling ? `${t.ruling} ${fmt(t.trial)}` : '—'} · practice{' '}
164                {t.audit === 'done' ? `${fmt(t.practice)}${t.standing ? ` ${t.standing}` : ''}` : t.audit}
165              </Text>
166              {t.auditError && <Text color="red">{'    '}{t.auditError}</Text>}
167            </Box>
168          ))}
169        </Box>
170        <Box marginTop={1}>
171          <Button key="refresh" onPress={() => refresh($, true)}>Refresh</Button>
172        </Box>
173        <Text dimColor>{BOARD_URL}</Text>
174      </Box>
175    )
176  })
177}
178
types/index.d.ts 31 lines
1/** One team on the Live Board, as the pane draws it. */
2export type TeamRow = {
3  key: string
4  team: string
5  trial: number | null
6  ruling: string | null
7  trialRunning: boolean
8  practice: number | null
9  standing: string | null
10  audit: 'done' | 'running' | 'queued' | 'error' | 'none'
11  auditError: string | null
12  delta: number | null
13}
14
15/** The last read of Judy's /api/hq, plus the nightly eval gates and whether the MLflow range is up. */
16export type Board = {
17  fetchedAt: number
18  updatedAt: string | null
19  teams: TeamRow[]
20  running: { audits: number; trials: number }
21  evals: string | null
22  range: boolean | null
23  error: string | null
24}
25
26declare module 'claude-code' {
27  interface PluginState {
28    'judy-board': { board: Board | null }
29  }
30}
31