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

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
| Mod | What it does |
|---|---|
judy-board | Judy'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-guard | Judy'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. |
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 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.⚖ 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.
| Rule | Refuses | Allowed route | |
|---|---|---|---|
| 1. The court code stays in the chat | Any command or file write whose text contains today's court code, in any case | Scripts read it from JUDY_EVENT_CODE or the Keychain | |
| 2. No secret printed into the transcript | security 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 gate | A 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-gateway | api.typesafe.ai, TYPESAFE_API_KEY, typesafe/jev or a typesafe:/ model in a command outside jev-gateway, or in a code file written outside it | POST /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.
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
hooks/register.tsx 178 lines1// 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 & 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}
178types/index.d.ts 31 lines1/** 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