Skip to content

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 isolated claude -p subprocess 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 one call row 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. parse means 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, one give-up row 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 a task-store row 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 (SourceFileLoader on kernel/judge.py) rather than inferring from logs.