A built-in design panel that reviews every generation before it ships. Five specialists (Designer, Critic, Brand, Accessibility, Copy) score the artifact across each round, and the run keeps iterating until the composite clears the threshold or the orchestrator gives up.
The product label is Design Jury. The internal feature name (code paths,
metrics, env vars) stays Critique Theater: apps/daemon/src/critique/,
apps/web/src/components/Theater/, SSE channels critique.*, env vars
OD_CRITIQUE_*. The user-facing label is sourced from a single i18n key,
critiqueTheater.userFacingName, so the product name can be renamed without
touching code.
When an agent emits an <artifact> block, the Critique Theater wraps the
artifact in a structured review cycle. Five panelists vote on independent
dimensions, the orchestrator composites their scores, and the run either
ships (composite β₯ threshold) or runs another round. The user sees:
- A live panel while the run is in flight: one lane per panelist with the current round's score, the cumulative must-fix count, and a sparkline of per-dim scores.
- A score ticker showing the composite trajectory across rounds, with a horizontal threshold marker so it is obvious whether the latest round cleared the bar.
- A collapsed badge once the run settles: "Shipped at round 2, composite
8.6" for the happy path, with status variants for
below_threshold,timed_out, andinterrupted.
The Theater runs inside the same CLI session the agent already uses (no second process, no second transport). All five panelists are turns in the same conversation, which keeps the model context coherent and prevents the "panelist disagrees with itself across processes" failure mode.
| Role | What it scores |
|---|---|
| Designer | Layout, composition, hierarchy. The "is this beautiful and balanced" lens. |
| Critic | Whether the artifact actually meets the brief. Contrast, weight, readability. |
| Brand | Token compliance, voice, on-brand color use. |
| Accessibility | WCAG conformance, focus rings, semantic structure, alt text. |
| Copy | Voice, tone, terseness, error message quality. |
The cast is fixed in v1. Per-skill cast configuration is reserved for v2 (see roadmap).
The orchestrator runs up to 3 rounds by default. After each round the composite is computed from the per-panelist weights:
composite = designer Γ 0.0
+ critic Γ 0.4
+ brand Γ 0.2
+ a11y Γ 0.2
+ copy Γ 0.2
(Designer is weighted at zero in v1 because their dimensions are aesthetic preferences rather than ship gates. The slot exists so the Designer's qualitative notes still travel into the transcript, and a future config release can bump the weight without changing the schema.)
If the composite meets the threshold (8.0 / 10) the run ships. Otherwise
the orchestrator emits the round summary, the agent revises, and the next
round begins. After 3 rounds without convergence the run takes the
fallbackPolicy path (ship_best by default, falling back to whichever
round had the highest composite).
The orchestrator does not spawn extra processes per panelist. Each panelist
is a turn in the same agent session, separated by <PANELIST role="...">
tags in the protocol stream. The parser converts those into panelist_*
events that the web reducer consumes via SSE. This keeps the operational
contract identical to a normal generation: same auth, same env, same logs.
The feature is gated by a four-tier resolver on the daemon side:
- Per-skill
od.critique.policy(highest priority). A skill that setspolicy: requiredforces the panel on for every generation that uses it;policy: opt-outforces it off;policy: opt-inlets the panel run only at M2 and above. - Per-project override. The web's
setCritiqueTheaterEnabledsetter (Phase 15.2) writes the toggle tolocalStoragefor the in-session UI and, when called with aprojectId, performs a read-merge-write through the existingPATCH /api/projects/:idsettings endpoint: GET the current project, mergecritiqueTheaterEnabledinto the existing metadata blob, PATCH the merged object so other metadata fields (kind,templateId,linkedDirs, etc.) survive. If the prefetch GET fails the setter skips the PATCH entirely instead of stomping the row. The daemon readsmetadata.critiqueTheaterEnabledon the next spawn. The dedicated Design Jury section inSettingsDialog.tsxcalls this project-aware setter for the active project; non-project embeds may still call the setter without aprojectIdfor session-local UI state. OD_CRITIQUE_ENABLEDenv override. Power-user lane / CI fixtures.- Rollout phase default (lowest priority). M0 / M1 =
false, M2 = true forpolicy: opt-inskills, M3 =trueeverywhere.
The web hook (useCritiqueTheaterEnabled) reads localStorage and
governs whether the Theater UI renders for the active session; the
daemon-side resolver makes the actual routing decision per
generation. The two layers share the same toggle but answer
different questions.
Defaults: disabled during M0 dark-launch and M1 settings-toggle phases.
Enabled by default per skill during M2, then globally during M3 after
β₯ 90% of production adapters maintain conformance for 14 consecutive days
(see docs/agent-adapters.md).
A skill can opt in or out via od.critique.policy in its SKILL.md
frontmatter:
od:
critique:
policy: required # or 'opt-in', 'opt-out'Skills that publish a deterministic artifact (e.g. od-export-pdf) usually
set opt-out; skills that generate net-new design output (magazine-poster,
saas-landing) set required.
The threshold (8.0) and per-role weights (designer 0 / critic 0.4 / brand 0.2 / a11y 0.2 / copy 0.2) are read-only in v1. v2 will expose them in the Settings panel.
After a run settles the Theater collapses to a single chip. The chip displays:
- composite β the final score on the 0 / 10 scale.
- round β which round closed the run.
- status badge β colored token explaining how the run ended:
Shipped: composite β₯ threshold (the happy path, accent-tinted).Below threshold: composite < threshold after all rounds (amber-tinted).Timed out: total run exceededtotalTimeoutMs(amber-tinted).Interrupted: user pressed Esc or clicked Interrupt mid-run.Degraded: the panel was offline this run (adapter could not produce a valid critique). See troubleshooting below.
The interrupted chip uses a distinct copy ("Interrupted at round N, best
composite X.X") so the user is not told the run shipped when it did not.
Every run writes a structured transcript (.ndjson, optionally .gz).
Opening the chip's Replay button mounts a read-only Theater that plays
the transcript at the chosen speed:
Instant: flushes every event synchronously. Useful when reopening a finished run.Live: paces events at the original cadence (Phase 7+ follow-up; v1 treats this the same as a 0-msintervalMs).{ intervalMs: N }: fixed N-ms delay between events.Paused: holds the cursor in place. Resuming picks up exactly where the pause landed (no re-flush of prior events).
The replay surface is keyboard-scrubbable: J / K jump round-by-round,
Esc exits the replay.
The orchestrator emits a degraded event when one of these happens:
| Reason | Cause | Remediation |
|---|---|---|
malformed_block |
The adapter emitted a <CRITIQUE> block the parser rejects. |
Re-run the conformance harness locally (pnpm --filter @open-design/daemon vitest run tests/critique-conformance.test.ts) to confirm the adapter's transcript shape. The Phase 12 dashboard surfaces this status as a Prometheus series once that PR lands; until then the harness is the authoritative source. |
oversize_block |
The block exceeded parserMaxBlockBytes. |
Usually a runaway model; retry once or raise the budget. |
adapter_unsupported |
The adapter is marked critique:degraded for the 24h TTL window. |
Wait for the TTL to elapse. The adapter-degraded registry exposes clearDegraded(adapterId) from apps/daemon/src/critique/adapter-degraded.ts for programmatic resets; a od adapters clear-degraded <id> CLI wrapper is planned in a follow-up. |
protocol_version_mismatch |
The adapter is on an older protocol. | Update the adapter or pin protocol negotiation. |
missing_artifact |
The run finished but no <artifact> body landed. |
Almost always a prompt bug; check the skill template. |
The run shipped under the bar with fallbackPolicy: ship_best. Tune the
brief (more specific guidance, tighter token constraints), switch to a
skill whose DESIGN.md aligns better with the brief, or raise maxRounds.
The user pressed Esc or clicked Interrupt. The chip surfaces the best composite seen so far. You can resume by re-issuing the brief.
Why five panelists, why fixed? The five-role cast is the smallest set that covers the lenses every artifact reviewer uses in practice (composition, intent, brand, accessibility, voice). Locking the set keeps the SSE wire shape, the SQLite schema, and the metrics dashboard stable. Per-skill cast configuration is reserved for v2.
Why is my adapter marked degraded for 24h? The conformance harness
runs every adapter prerelease against 10 brief templates. If an adapter
drops under the 90% shipped or 95% clean-parse thresholds for two
consecutive cycles, it gets marked critique:degraded for 24h. The mark
auto-clears on the next clean cycle.
Can I add my own panelist? Not in v1. v2 plans a panelist plug-in
contract; see docs/roadmap.md for the timeline.
docs/spec.mdβ protocol v1 wire format.docs/architecture.mdβ orchestrator + parser layout.docs/skills-protocol.mdβod.critique.policyfrontmatter contract.docs/agent-adapters.mdβ conformance contract and adapter responsibilities.docs/roadmap.mdβ v2 panelist extensions.apps/daemon/src/critique/AGENTS.mdβ daemon-side module map.apps/web/src/components/Theater/AGENTS.mdβ web-side module map.