The judges: a field guide¶
Optional reading
You don't need any of this to use Romp. The Internals section is here for when you're curious how the task layer works under the hood. Describes the system as of 2026-07-20; the behaviour it documents moves, so treat anything here as a snapshot rather than a contract.
The picture first: judge-pipeline.md is the one-page diagram map (when each judge runs, card-first filing, the state machine, the postal flow). This page is the per-judge detail behind it, plus the failure contract. The state model itself (the diary, the fold, every chip) lives in goal-state.md.
romp keeps two live artifacts per session without you curating either: a readable text record (chat captions, the session TOC, the timeline) and the goal board (the cards on the feed). Judges maintain both. A judge is one small LLM call with one narrow job, one system prompt, and one name. It fires on an event in the session's life, writes its verdict to an append-only record, and plain code does the rest. No judge owns state: a wrong verdict is one bad event in a log, outvoted by later evidence, never a corrupted fact.
The thirteen judges run in two tiers. The index tier (cheap, fast models) writes text only and never touches goals. The triage tier (a stronger model) maintains the board. Both run continuously from the kernel producer and are event-gated, so idle sessions cost file stats, not model calls.
The roster¶
| Judge | Tier | Prompt | Fires when |
|---|---|---|---|
| captioner | index | CAPTION_SYS |
a segment or turn's work ends |
| gister | index | GIST_SYS |
a user message lands |
| archiver | index | ARCHIVE_SYS |
a session gains a turn |
| opener | triage | OPENER_SYS |
a message lands, work still running |
| planner | triage | PLAN_SYS |
a segment's work ends |
| placer | triage | PLACE_SYS |
the planner filed under a card with open sub-goals |
| closer | triage | CLOSER_SYS |
a turn ends |
| unblocker | triage | UNBLOCK_SYS |
an open blocked sub has ended turns newer than its block (or its last check) |
| distiller | triage | DISTILL_SYS |
a card completed and settled |
| briefer | triage | BLOCK_BRIEF_SYS |
a card blocked |
| grouper | triage | GROUP_SYS |
the set of open cards changed |
| consolidator | triage | GROUP_SYS (shared) |
the set of completed cards changed |
| courier | triage | COURIER_SYS |
a peer message arrives |
Twelve prompts back the thirteen names: the consolidator reuses the
grouper's prompt over a different column, under its own name. Usage and error logs
carry one name per prompt; the timeline band keeps five family rows and
folds the fine names onto them (_JUDGE_FAMILY in kernel/kernel.py).
Prompts are named by their constant in kernel/judge.py; grep the constant,
line numbers drift.
A turn through the judges¶
A segment is one input and the work it causes; a turn holds one or more segments (your message, then perhaps a peer's, each with its own work).
The moment your message lands, the gister captions what it is about and the opener puts the ask on the board, before any work exists. When a segment's work ends, the captioner writes its one finished line and the planner files what the work actually did, calling the placer when a spot inside a card needs picking. When the turn ends, the closer audits the goals the turn touched for quiet completions and the archiver refreshes the session's headline. From there the board keeps itself: a completed card settles and gets the distiller's takeaway, a blocked card gets the briefer's decision brief, a blocked sub with new conversation since its block gets the unblocker's re-examination (was its question answered in passing?), a changed set of top cards may get nested by the grouper (open column) or the consolidator (done column), and a peer message goes to the courier instead of the planners. Every section below is one of those moments in detail.
The index tier: the text record¶
Three index-tier judges write the words you read in the chat, the timeline, and search. They are write-only: nothing they emit changes a card's state.
captioner. The readable activity log. Per finished segment or turn, one
short past-tense phrase (4 to 7 words) that leads with the result and never
names a tool. An empty reply means "no finished work", skip. Appends to
captions/<fsid>.jsonl; feeds the chat, the feed cards, the timeline.
gister. The captioner's sibling for a request still in progress: a present-tense topic phrase ("a dark-mode toggle for settings"), not a result. Feeds the "Analyzing:" placeholder card and the timeline dot the moment a message lands.
archiver. The per-session headline and abstract, re-run when the
session gains a turn. Reads the session's turn captions oldest-first;
replies exactly two lines, HEADLINE: and ABSTRACT:. Written to
archive/<fsid>.json; feeds the chat TOC and the search index.
Filing: the opener, the planner, the placer¶
Filing answers one question: which card does this belong to? Three judges answer it at different moments. The opener places your ask the instant it lands. The planner places the finished work and rules on it. The placer picks the depth inside a card when that is still open.
All three file card-first. The open-goal menu renders as a tree grouped under top-level cards, and filing names a card, never a nested line; the test is "can this card be called done without this work?", judged where you actually experience the board. Only the placer ever goes deeper than the card.
opener. Fires the moment your message lands on a still-open segment, so the board shows the ask before the work exists. It is the closer's mirror: it may only open (mint a new card or file under an open one, card level, never done or block), as the closer may only close. A reply that never parses is logged and the ask is hard-placed anyway; a prompted goal never stays unplaced.
planner. Fires when a segment's work ends, and holds the full op list:
mint, sub, done, block, retitle, skip. Done is eager (an answer
counts as done), but ending by asking you to approve is a block, and only
the human blocks: peer, CI, and build waits stay working. Its verdicts
append diary events. The same engine, mode-switched by note injections,
handles four more phases: a live re-plan after you clear a card mid-work,
nudge resolution (resolve the named goal, done or block, no plain step),
delegation follow-on (file the recipient's work under the courier's plant),
and tagged follow-ups (file under the cited goal unless the reply starts a
different thread — and even then the new goal groups with the cited card
under one umbrella: the follow-up tie). A segment opened by an
untargeted kernel notice (restart or resume) carries a housekeeping note:
pure verification sweeps file nothing.
placer. The second, scoped call, only when the chosen card already has open sub-goals: it sees just that card's subtree and picks the spot, biased to the highest level that makes sense. Most cards have no open sub-goals, so most placements stay one call; the opener's and the live re-plan's placements always stay card level.
None of the three reorganizes the board; that is the grouper's job, and the prompts say so.
Status and summaries: the closer, the unblocker, the distiller, the briefer¶
A card should say so when it is finished or stuck, without the agent having to narrate it. The closer supplies the missing verdicts; the unblocker retires the stale ones; the distiller and the briefer write what you read on the resolved card.
closer. The turn-end completion backstop; it exists because agents
rarely say "done". It audits only the goals the turn actually touched;
verdict done, blocked, or omit, with "when in doubt, omit". Idempotent per
turn. Its diary events carry src closer, so planner and closer verdicts
stay distinguishable, and both defer to the user floor: a verdict computed
from evidence at or before your last reply loses.
unblocker. The stale-block backstop; it exists because answers arrive
in passing. A sub-goal blocked on a question is only ever unblocked by
work filed on that exact node — but the answer usually files wherever the
planner judges the segment to serve, so a dormant blocked sub never hears
it and holds its whole card in Needs you (a card can otherwise sit for hours
on a buried sub whose question the very next stretch of conversation already
answered). Given each open blocked sub's question plus
the conversation since its block, it verdicts lift or hold, "when unsure,
hold"; a lift lands as a normal unblock diary event, why-prefixed
"answered in passing". Subs only — a blocked top is the card's Needs you,
with its own heal paths (your reply re-judges it; a placement under it
unblocks the chain). Event-gated per node: blockCheckT remembers the
newest ended turn examined, so a stable session costs zero calls and a
give-up re-arms on the next new turn. Its model call spans seconds, so it
holds no store copy across the call: verdicts apply to a fresh load, and a
node that moved on mid-call (you resolved or cleared it) is skipped with a
drift-skip error row rather than overwritten. ROMP_UNBLOCKER=0
disables.
distiller. When a top card completes and settles: BACKGROUND:
(re-orientation for a reader who lost the thread) plus TAKEAWAY: (the one
thing you would most want to know now that it is done), consuming the
closer's done-reason as ground truth. After a follow-up re-completes a
card, the prior summary is handed back and the work text is cut to the
stretch after your follow-up, so the takeaway is the update, never a recap.
May cite a SOURCE: mN line, parsed into the summary's deep link; a cite
that misses logs and chips the card instead of failing.
briefer. When a top card blocks (and live for the focused picker or
permission goal): a decision brief that leads with exactly what you must
decide or provide, then options and tradeoffs. Same SOURCE: contract as
the distiller.
Board shape: the grouper and the consolidator¶
As cards accumulate, related tops nest under umbrellas so the board stays glanceable. Both judges move whole subtrees and append no diary events: structure, never status.
grouper. Given the open top-level cards: nest one under another, or
mint an umbrella when several serve one outcome, and "doing nothing is a
valid, common outcome". Called only when the open-top set actually changed.
Hard rules in apply_group: never touch a view-cleared card, no cycles,
depth clamp 4, same-session only. A to-do-mirror top (planted flat by
plan-sync) is explicitly the grouper's to nest.
consolidator. The same prompt over the completed column: it groups related all-completed sibling tops under a done umbrella. It exists because the grouper only ever sees open tops, so related goals that finish before they get grouped would pile up flat in Completed forever, and an umbrella minted over already-done tops adopts nothing. Safe by construction: every child is done, so the umbrella rolls up completed. Gated by its own signature, logged under its own name.
Peer mail: the courier¶
The courier owns peer-message segments; the planners skip them. The sender
declared each message delegate, coordinate, or question at send time
(schema-required); the courier takes that as a strong prior and reads the
body for whether work actually changed hands. Delegating plants a real goal
in the recipient's tree (origin-stamped) plus a "delegated to" tracking
node in the sender's; coordinating makes no card. The companion
run_propagate is deterministic: when the recipient completes the plant,
the sender's tracker checks itself off through the origin pointer.
When a judge fails¶
Every failure logs one row to judge-errors.jsonl that answers who, where,
what, and why on its own: judge (the one-per-prompt name),
fsid, err (the kind), and note (the evidence: a reply tail, the API's
own error message, an exception name, or a give-up's scope and re-arm
event).
- A failed call is not a parse failure. Every call goes through one
entrypoint,
_judge_run: an isolatedclaude -psubprocess with a hard timeout. That entrypoint owns all call-level logging: a dead subprocess, a timeout, or an API error envelope (the CLI answering with an error string instead of a model reply) logs onecallrow and hands the caller an empty reply. Error text never reaches a parser. One API incident once became 2,352 phantom "parse" errors in a single hour because every parser rejected the same error message and every caller retried; this rule is why that cannot recur. - An empty reply never counts.
parsemeans the model's own text was rejected, and the row carries the reply tail so the log says why. Empty replies (the rate gate, a failed call) log nothing at the caller and never burn a retry cap. - Every judge is capped. Three genuine parse rejects on the same work
item (
JUDGE_FAIL_CAP) and the judge gives up loudly, onegive-uprow naming the re-arm event, instead of retrying every pass forever:
| Judge | On failure | Re-arms |
|---|---|---|
| opener, live re-plan | hard-places at card level immediately | (no retries needed) |
| planner (work run) | 3 tries, then hard-place a user message / drop a non-user segment | on its next segment |
| placer | files at the card immediately | (no retries needed) |
| closer | 3 tries, then skips the turn | when the turn gains atoms |
| archiver | 3 tries, then keeps serving the old headline | when the session gains a turn |
| grouper, consolidator | 3 tries, then leaves the board shape as is | when the top set changes |
| courier | 3 tries, then resolves from the sender's declared kind | (terminal; never orphans a message) |
| distiller, briefer | 3 tries, then a blank summary plus a card warn | on recovery, or a fresh completion/block |
While the account usage window is exhausted, the rate gate skips every call
across every session and logs one rate-limited row per window; gate skips count
toward nothing.
Other machinery that reads the same data¶
- rollup_status: pure code. Folds each node's diary into its state and each card's subtree into a column (see goal-state.md). Self-healing, and holds the authoritative tier: an open item on the agent's own to-do list pins the card in Working over any judge verdict.
- plan-sync: pure code. Mirrors the agent's own to-do list as flat top
cards ("declared in the agent's own to-do list"); the grouper nests them.
It reads the live task store (
~/.claude/tasks/<fsid>/, the same source the chat TO-DO card reads) — never the transcript, whose record of a TaskUpdate can fall off the live chain when an api-error retry forks the graph. A missing store falls back to the transcript fold; an unreadable one logs atask-storerow and skips the pass rather than silently degrading. - auto-nudge: a kernel trigger, not an LLM. Detects a genuinely stalled session and injects one nudge prompt; the planner's nudge phase does the judging, and a failed nudge records the block.
- awaiting: event-derived, never a verdict. Only real subagents make an idle session awaiting.
Where responsibilities overlap¶
- planner vs closer on done/block: by design. The planner is eager per segment (precision), the closer is the turn-end backstop (recall); diary src tells them apart, and both yield to the user floor.
- opener vs planner: the same segment, two moments. The opener's placement is the instant guess; the planner's work run may correct it (retitle, refine) but never duplicates it.
- grouper vs consolidator: same prompt, disjoint columns, separate names in the logs.
- distiller vs closer: consumer relationship; the distiller treats the closer's done-reason as ground truth.
- courier vs planner: mutually exclusive by segment author; the courier plants, the planner's delegation phase files under the plant.
Ops and knobs¶
- Toggles:
CLOSER_ON,GROUPER_ON,DISTILLER_ON,CONSOLIDATE_ON. Models:STATE/judge-model(triage),STATE/index-model. - Logs:
STATE/judge-usage.jsonl(per-call cost, one name per prompt),STATE/judge-errors.jsonl(the row contract above; kinds are parse, call, give-up, cite-miss, rate-limited, task-store, history-unreadable). - Debugging: run the judge's own code against the live store
(
SourceFileLoaderonkernel/judge.py) rather than inferring from logs.