@agentsdance/codejury 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ When that ref is missing, Jury discovers `refs/merge-requests/<shard>/<id>/<revi
28
28
  refs and fetches the highest numeric revision, verifying the advertised commit.
29
29
  The shard is discovered independently of the MR number. If
30
30
  the source branch is not uniquely visible on the target remote (commonly a
31
- fork), use `--no-push` for a read-only review or check out the source branch
31
+ fork), check out the source branch
32
32
  locally, omit the MR URL, and run
33
33
  `jury --dir /path/to/checkout --trunk <target-branch>`.
34
34
 
@@ -86,37 +86,52 @@ Two properties make it terminate rather than churn:
86
86
 
87
87
  ## Watching it happen
88
88
 
89
- A round is a reviewer talking for several minutes and then a wall of text. `--web` starts the console
89
+ A round is a reviewer talking for several minutes and then a wall of text. Reviews start the console by default
90
90
  in the same process as the run, so the conversation streams as it is spoken rather than arriving at
91
91
  the end:
92
92
 
93
93
  ```
94
- jury --web https://github.com/owner/repo/pull/1 # review, with the console open on it
95
- jury --web --port 3099 --rounds 3 # the current branch, on another port
94
+ jury https://github.com/owner/repo/pull/1 # review, with the console open on it
95
+ jury --port 3099 --rounds 3 # the current branch, on another port
96
96
  ```
97
97
 
98
+ Use `--push=false` to keep fixes local; pushing is enabled by default.
99
+
100
+ Use `--web=false` (or `--web false`) to disable the console and exit when review finishes.
101
+ `--web`, `--web=true`, and `--web true` enable it explicitly.
102
+
103
+ The browser opens with `?run=<current-run>` so it shows the review just started,
104
+ including when the server falls back to another port.
105
+
98
106
  The console outlives the loop — it stays up until you ctrl-c, which is the point: the run finishing
99
107
  is when there is finally something worth reading. `jury web` still serves the same console
100
108
  standalone, against runs that already exist.
101
109
 
102
110
  ## Who writes, and who only reads
103
111
 
104
- Exactly one agent has role `main`; it is the default **judge**. The built-in default is Claude Code.
112
+ Exactly one agent has role `main`; it is the default **judge**. The built-in default is Codex.
105
113
  The judge owns the working tree and commit, triages every finding, and is the only writer. Choose a
106
- different enabled agent for one run with `--judge`; without that flag Claude remains the default:
114
+ different enabled agent for one run with `--judge`; without that flag the configured main agent is used (Codex by default):
107
115
 
108
116
  ```bash
109
- jury https://github.com/owner/repo/pull/1 # Claude judges
110
- jury --judge codex https://github.com/owner/repo/pull/1 # Codex judges
117
+ jury https://github.com/owner/repo/pull/1 # Codex judges
118
+ jury --judge claude https://github.com/owner/repo/pull/1 # Claude judges
111
119
  ```
112
120
 
121
+ When upgrading from 0.1.x, a configuration that disables Codex or marks it as a reviewer
122
+ needs an explicit judge: use `--judge claude`, or assign role `main` to the intended
123
+ agent in `jury.config.json`. For scripts, use `--web=false` to exit after review.
124
+
113
125
  `--judge` accepts exactly one agent name. The selected judge is removed from that run's reviewer
114
- pool, so it never reviews its own work.
126
+ pool, so it never reviews its own work. Codex uses a read-only sandbox for reviews
127
+ and a workspace-write sandbox for judging. Claude uses plan mode for reviews and
128
+ acceptEdits for judging. Custom agent `argv` remains authoritative; `judgeArgv` can
129
+ provide a separate judge command when needed.
115
130
  `jury agents` shows the configured roles:
116
131
 
117
132
  ```
118
- ok claude main /usr/local/bin/claude
119
- ok codex reviewer /usr/local/bin/codex
133
+ ok claude reviewer /usr/local/bin/claude
134
+ ok codex main /usr/local/bin/codex
120
135
  ok agy reviewer /usr/local/bin/agy
121
136
  ```
122
137
 
@@ -127,7 +142,7 @@ The judge then holds **one conversation per reviewer**, concurrently, each about
127
142
  own findings and nothing else:
128
143
 
129
144
  ```
130
- claude ──▶ codex its 5 findings, resumed session
145
+ codex ──▶ claude its 5 findings, fresh run with its own words quoted back
131
146
  └──▶ agy its 5 findings, fresh run with its own words quoted back
132
147
  ```
133
148
 
package/bin/jury.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // jury — run a pull request past several independent AI reviewers.
2
+ // jury — review a pull request with multiple independent AI reviewers.
3
3
  //
4
4
  // The CLI owns the mechanics: worktrees, spawning agents, capturing what they
5
5
  // said, recording it, serving the console. It deliberately does NOT triage —
@@ -22,6 +22,8 @@ import { triageOne } from "../lib/triage.js";
22
22
  import { assertPrCheckout, repositoryFromPrUrl, resolvePrCheckout } from "../lib/repository.js";
23
23
  import { resolveJuryDirectory } from "../lib/directories.js";
24
24
  import * as st from "../lib/style.js";
25
+ import { parseReviewArgs } from "../lib/cli-options.js";
26
+ import { startReviewConsole } from "../lib/review-console.js";
25
27
 
26
28
  const run = promisify(execFile);
27
29
  // Read from the manifest rather than restated here, where it drifted: the CLI
@@ -43,19 +45,20 @@ let resolvedCheckoutCleanup = null;
43
45
  * of it by default buried the one command that matters in a wall of options —
44
46
  * `jury help --all` still prints everything.
45
47
  */
46
- const USAGE = `jury — run a pull request past several AI reviewers until they agree
48
+ const USAGE = `jury — review a pull request with multiple AI reviewers until they agree
47
49
 
48
50
  jury <pr-url> review a pull request
49
- jury --rounds 3 review the current branch, no PR
50
- jury --web <pr-url> …and watch it in the browser
51
+ jury <pr-url> --rounds 3 review a pull request for up to 3 rounds
52
+ jury <pr-url> --web=false review without the browser console
51
53
 
52
54
  Common flags
53
55
 
54
- --dir <path> working/state root (default: Git cwd or ~/.jury)
55
- --rounds <n> stop after n rounds (default 10)
56
- --agents codex,grok only these reviewers (default: all installed)
57
- --judge codex one agent that triages and fixes (default: claude)
58
- --no-push fix locally, do not push
56
+ --dir <path> working/state root (default: Git cwd or ~/.jury)
57
+ --rounds <n> stop after n rounds (default: 10)
58
+ --agents claude,grok only these reviewers (default: configured reviewers)
59
+ --judge codex one agent that triages and fixes (default: codex)
60
+ --push <true|false> commit and push fixes (default: true)
61
+ --web <true|false> open the browser console (default: true)
59
62
 
60
63
  Other commands
61
64
 
@@ -69,55 +72,55 @@ Other commands
69
72
 
70
73
  const USAGE_FULL = `jury — multi-agent code review
71
74
 
72
- jury <pr-url> review a PR until every reviewer approves, one conversation per reviewer
73
- jury review-once [flags] a single round, no triage or reply
74
- jury web [flags] serve the console (default http://127.0.0.1:3080)
75
- jury finding <cmd> list | reproduce | resolve | settled — appends events, enforces the gate
76
- jury reply [flags] send each reviewer your verdicts on ITS findings, one conversation each
77
- jury runs list every PR under review, with its slug for --run
78
- jury agents check which configured agents are installed
75
+ jury <pr-url> review a PR until every reviewer approves, one conversation per reviewer
76
+ jury review-once [flags] a single round, no triage or reply
77
+ jury web [flags] serve the console (default http://127.0.0.1:3080)
78
+ jury finding <cmd> list | reproduce | resolve | settled — appends events, enforces the gate
79
+ jury reply [flags] send each reviewer your verdicts on ITS findings, one conversation each
80
+ jury runs list every PR under review, with its slug for --run
81
+ jury agents check which configured agents are installed
79
82
  jury version
80
83
 
81
- review (drives itself; no operator between rounds)
84
+ review (triages, fixes, commits, and pushes automatically)
82
85
  jury https://github.com/owner/repo/pull/1
83
- jury --rounds 3 the current branch, no PR
84
-
85
- --dir <path> repo/worktree (default: cwd if Git, otherwise ~/.jury)
86
- --pr <url> same as the positional argument
87
- --trunk <branch> diff base branch (default: the remote's own HEAD)
88
- --title <text> what the change does (default: read from the PR)
89
- --summary <text> intent, passed to reviewers (default: the PR description)
90
- --rounds <n> maximum rounds (default 10)
91
- --agents a,b only these reviewers (default: all installed)
92
- --judge <agent> one agent that triages and fixes (default: claude)
93
- --resume <slug> continue an existing run instead of starting a new one
94
- --web open the console on this run (stays up when it ends)
95
- --port <n> console port, with --web (default 3080)
96
- --no-push commit fixes to the worktree without pushing
97
- --dry-run (internal) exercise the pipeline, spawn no agents. Always
98
- reports clean and triages nothing, so it says whether the
99
- plumbing runs and never whether the code is good.
86
+ jury <pr-url> --rounds 3 review a pull request for up to 3 rounds
87
+
88
+ --dir <path> repo/worktree (default: cwd if Git, otherwise ~/.jury)
89
+ --pr <url> same as the positional argument
90
+ --trunk <branch> diff base branch (default: the remote's own HEAD)
91
+ --title <text> what the change does (default: read from the PR)
92
+ --summary <text> intent, passed to reviewers (default: the PR description)
93
+ --rounds <n> maximum rounds (default 10)
94
+ --agents a,b only these reviewers (default: configured reviewers)
95
+ --judge <agent> one agent that triages and fixes (default: codex)
96
+ --resume <slug> continue an existing run instead of starting a new one
97
+ --web <true|false> open the console; stays up after review (default: true)
98
+ --port <n> console port (default 3080)
99
+ --push <true|false> commit and push fixes (default: true)
100
+ --dry-run (internal) exercise the pipeline, spawn no agents. Always
101
+ reports clean and triages nothing, so it says whether the
102
+ plumbing runs and never whether the code is good.
100
103
 
101
104
  review-once flags
102
- --dir <path> repo/worktree (default: cwd if Git, otherwise ~/.jury)
103
- --pr <url> pull request URL, recorded on the run
104
- --title <text> what the change does, shown in the console
105
- --summary <text> a few lines of intent, passed to reviewers
106
- --trunk <branch> diff base branch (default master)
107
- --round <n> round number (default: next)
108
- --agents a,b only these reviewers (default: all enabled)
109
- --max-rounds <n> keep going until every reviewer approves, at most n (default 1)
105
+ --dir <path> repo/worktree (default: cwd if Git, otherwise ~/.jury)
106
+ --pr <url> pull request URL, recorded on the run
107
+ --title <text> what the change does, shown in the console
108
+ --summary <text> a few lines of intent, passed to reviewers
109
+ --trunk <branch> diff base branch (default master)
110
+ --round <n> round number (default: next)
111
+ --agents a,b only these reviewers (default: all enabled)
112
+ --max-rounds <n> keep going until every reviewer approves, at most n (default 1)
110
113
 
111
114
  web flags
112
- --dir <path> run-state root (default: cwd if Git, otherwise ~/.jury)
113
- --port <n> default 3080, walks forward if busy
114
- --open open a browser
115
+ --dir <path> run-state root (default: cwd if Git, otherwise ~/.jury)
116
+ --port <n> default 3080, walks forward if busy
117
+ --open open a browser
115
118
 
116
119
  finding commands (--dir picks state root; --run picks run)
117
120
  jury finding list
118
121
  jury finding reproduce <id> --evidence <text> [--test <text>]
119
122
  jury finding resolve <id> --verdict <${VERDICTS.join("|")}> [--reason <text>] [--test <text>]
120
- jury finding settled print the regenerated settled list
123
+ jury finding settled print the regenerated settled list
121
124
 
122
125
  state commands
123
126
  jury runs [--dir <path>]
@@ -496,24 +499,19 @@ async function runRound({ dir, round, pool, cfg, target, worktree, values, sha,
496
499
  async function cmdAgent(argv) {
497
500
  const judgeFlags = argv.filter((a) => a === "--judge" || a.startsWith("--judge="));
498
501
  if (judgeFlags.length > 1) throw new Error("--judge accepts exactly one agent");
499
- const { values, positionals } = parseArgs({
500
- args: argv, allowPositionals: true,
501
- options: {
502
- dir: { type: "string" },
503
- pr: { type: "string" },
504
- title: { type: "string" },
505
- summary: { type: "string" },
506
- trunk: { type: "string" },
507
- rounds: { type: "string", default: "10" },
508
- agents: { type: "string" },
509
- judge: { type: "string" },
510
- push: { type: "boolean", default: true },
511
- "no-push": { type: "boolean", default: false },
512
- resume: { type: "string" },
513
- "dry-run": { type: "boolean", default: false },
514
- web: { type: "boolean", default: false },
515
- port: { type: "string", default: "3080" },
516
- },
502
+ const { values, positionals } = parseReviewArgs(argv, {
503
+ dir: { type: "string" },
504
+ pr: { type: "string" },
505
+ title: { type: "string" },
506
+ summary: { type: "string" },
507
+ trunk: { type: "string" },
508
+ rounds: { type: "string", default: "10" },
509
+ agents: { type: "string" },
510
+ judge: { type: "string" },
511
+ push: { type: "boolean", default: true },
512
+ resume: { type: "string" },
513
+ "dry-run": { type: "boolean", default: false },
514
+ port: { type: "string", default: "3080" },
517
515
  });
518
516
  if (values.judge?.includes(",")) throw new Error("--judge accepts exactly one agent, not a list");
519
517
 
@@ -532,7 +530,6 @@ async function cmdAgent(argv) {
532
530
  throw new Error("--rounds must be a positive integer");
533
531
  }
534
532
 
535
- values.push = values.push && !values["no-push"];
536
533
  const requestedWorktree = await commandDirectory(values.pr ? (values.dir ?? "") : values.dir);
537
534
  // Two different questions, and conflating them cost the local path entirely.
538
535
  // `root` is where the isolated checkout and run records are written; `dir` is
@@ -615,7 +612,11 @@ async function cmdAgent(argv) {
615
612
  if (requestedJudge) {
616
613
  throw new Error(`judge "${requestedJudge}" is not an enabled configured agent — available: ${available}`);
617
614
  }
618
- throw new Error('no agent has role "main" — configure one or pass --judge <agent>');
615
+ throw new Error('no agent has role "main" — Codex is the default judge; enable it or pass --judge <agent>');
616
+ }
617
+ if (!values["dry-run"]) {
618
+ const installed = await probe(judge);
619
+ if (!installed.ok) throw new Error(`judge "${judge.name}" is not installed (${installed.bin}); install it or select --judge <agent>`);
619
620
  }
620
621
  target.judge = judge.name;
621
622
 
@@ -641,10 +642,12 @@ async function cmdAgent(argv) {
641
642
  // directory and nothing else: the server re-reads it per request and tails
642
643
  // the event log, so it sees each round land without the loop telling it.
643
644
  if (values.web) {
644
- const { url } = await serve({ port: Number(values.port), cwd: requestedWorktree, onLog: (m) => console.log(st.field("console", st.muted(m))) });
645
- liveConsole = url;
646
- console.log(st.field("console", `${url}${st.muted(" → the conversation streams live")}`));
647
- openBrowser(url);
645
+ liveConsole = await startReviewConsole({
646
+ dir, target, cwd: requestedWorktree, port: Number(values.port),
647
+ onLog: (m) => console.log(st.field("console", st.muted(m))),
648
+ });
649
+ console.log(st.field("console", `${liveConsole}${st.muted(" → the conversation streams live")}`));
650
+ openBrowser(liveConsole);
648
651
  console.log("");
649
652
  } else {
650
653
  console.log(st.field("watch", st.muted("jury web → the conversation streams live")) + "\n");
@@ -4,7 +4,7 @@
4
4
  "and gitignored: which reviewers you have installed is a property of your box,",
5
5
  "not of the project.",
6
6
  "",
7
- "Built-in defaults already cover claude (main), codex and droid, so this file is",
7
+ "Built-in defaults already cover codex (main), claude and droid, so this file is",
8
8
  "only needed to add an agent or disable one.",
9
9
  "",
10
10
  "Placeholders: {{worktree}} {{promptFile}} {{promptText}} {{sha}}"
@@ -0,0 +1,21 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ /** Accept default-on boolean flags with optional true/false values. */
4
+ export function parseReviewArgs(args, options) {
5
+ const { tokens } = parseArgs({ args, options: { ...options, web: { type: "boolean" }, push: { type: "boolean" } },
6
+ allowPositionals: true, strict: false, tokens: true });
7
+ const normalized = [...args];
8
+ for (const token of tokens) {
9
+ if (token.kind !== "option" || !["web", "push"].includes(token.name) || token.inlineValue) continue;
10
+ const next = tokens.find((t) => t.index === token.index + 1);
11
+ if (next?.kind === "positional" && ["true", "false"].includes(next.value)) continue;
12
+ normalized[token.index] = `--${token.name}=true`;
13
+ }
14
+ const parsed = parseArgs({ args: normalized, allowPositionals: true,
15
+ options: { ...options, web: { type: "string", default: "true" }, push: { type: "string", default: "true" } } });
16
+ for (const name of ["web", "push"]) {
17
+ if (!["true", "false"].includes(parsed.values[name])) throw new Error(`--${name} must be true or false`);
18
+ parsed.values[name] = parsed.values[name] === "true";
19
+ }
20
+ return parsed;
21
+ }
package/lib/config.js CHANGED
@@ -15,37 +15,32 @@ export const DEFAULTS = {
15
15
  {
16
16
  name: "claude",
17
17
  product: "Anthropic Claude Code",
18
- role: "main",
19
- // The main agent used to carry empty argv, on the theory that it was the
20
- // session driving the loop rather than something spawned. That made the
21
- // loop unable to run without a person in the chair: nothing triaged, so
22
- // nothing was ever fixed and every round re-read the same commit. It is
23
- // spawned like any other agent now — the difference is that it is the
24
- // only one permitted to touch the tree.
18
+ role: "reviewer",
25
19
  promptDelivery: "argv",
26
20
  cwd: "worktree",
27
21
  argv: [
22
+ "claude", "-p", "{{promptText}}",
23
+ "--permission-mode", "plan",
24
+ "--disallowedTools", "Edit,Write,NotebookEdit",
25
+ "--add-dir", "{{worktree}}",
26
+ ],
27
+ judgeArgv: [
28
28
  "claude", "-p", "{{promptText}}",
29
29
  "--permission-mode", "acceptEdits",
30
30
  "--add-dir", "{{worktree}}",
31
31
  ],
32
32
  resume: { supported: false, reason: "each finding is judged on its own merits" },
33
33
  report: "whole",
34
- // Triage is the longest step in a round: read, reproduce, fix, run the
35
- // suite. It was 0 back when this agent was never spawned, which made its
36
- // timeout 0 and killed it the instant it started.
37
34
  expectSeconds: 900,
38
- notes:
39
- "Owns the working tree and the commit. Reproduces a finding before fixing it, proves the " +
40
- "regression test fails without the fix, and replies to every finding including rejections.",
41
35
  },
42
36
  {
43
37
  name: "codex",
44
38
  product: "OpenAI Codex",
45
- role: "reviewer",
39
+ role: "main",
46
40
  promptDelivery: "argv",
47
41
  cwd: "worktree",
48
- argv: ["codex", "exec", "--skip-git-repo-check", "{{promptText}}"],
42
+ argv: ["codex", "exec", "--sandbox", "read-only", "--skip-git-repo-check", "{{promptText}}"],
43
+ judgeArgv: ["codex", "exec", "--sandbox", "workspace-write", "--skip-git-repo-check", "{{promptText}}"],
49
44
  // `--last` means "whatever session ran most recently in this worktree",
50
45
  // which is only the review being answered if nothing else ran in between.
51
46
  // Any codex session started between the review and the reply steals the
@@ -123,9 +118,18 @@ export async function loadConfig(dir = process.cwd()) {
123
118
  }
124
119
 
125
120
  const byName = new Map(DEFAULTS.agents.map((a) => [a.name, { ...a }]));
121
+ const explicitMain = (user.agents ?? []).find((a) => a.role === "main" && a.enabled !== false);
122
+ if (explicitMain) {
123
+ for (const agent of byName.values()) {
124
+ if (agent.role === "main" && agent.name !== explicitMain.name) agent.role = "reviewer";
125
+ }
126
+ }
126
127
  for (const a of user.agents ?? []) {
127
128
  if (!a.name) throw new Error(`each agent in ${found} needs a name`);
128
- byName.set(a.name, { ...(byName.get(a.name) ?? {}), ...a });
129
+ const merged = { ...(byName.get(a.name) ?? {}), ...a };
130
+ // A custom executable must also be used for judging unless explicitly overridden.
131
+ if (a.argv && !Object.hasOwn(a, "judgeArgv")) delete merged.judgeArgv;
132
+ byName.set(a.name, merged);
129
133
  }
130
134
 
131
135
  const agents = [...byName.values()].filter((a) => a.enabled !== false);
@@ -149,8 +153,8 @@ export async function loadConfig(dir = process.cwd()) {
149
153
 
150
154
  /** Select the agent that judges findings and owns the working tree. */
151
155
  export function judgeAgent(cfg, name) {
152
- if (name) return cfg.agents.find((a) => a.name === name) ?? null;
153
- return cfg.main ?? null;
156
+ const agent = name ? cfg.agents.find((a) => a.name === name) : cfg.main;
157
+ return agent ? { ...agent, argv: agent.judgeArgv ?? agent.argv } : null;
154
158
  }
155
159
 
156
160
  export function reviewers(cfg) {
package/lib/repository.js CHANGED
@@ -330,7 +330,7 @@ async function resolveMergeRequestCheckout(prUrl, target, number, {
330
330
  );
331
331
 
332
332
  // Only when a push target is actually needed. This ran unconditionally, so
333
- // a --no-push review with every commit already on disk still failed the
333
+ // a resolution with allowPush false and every commit on disk still failed the
334
334
  // moment the network was unreachable — the one case the local path exists
335
335
  // to serve.
336
336
  let branch = null;
@@ -341,7 +341,7 @@ async function resolveMergeRequestCheckout(prUrl, target, number, {
341
341
  if (allowPush && !branch) {
342
342
  throw new MergeRequestCheckoutError(
343
343
  `Can't determine where to push fixes for merge request !${number}.\n` +
344
- "Add --no-push to review it without pushing changes.",
344
+ "Check out the source branch and run jury --dir <checkout> --trunk <target-branch> without the MR URL.",
345
345
  );
346
346
  }
347
347
 
@@ -442,7 +442,7 @@ export async function resolvePrCheckout(prUrl, {
442
442
  if (allowPush && details.state !== "OPEN") {
443
443
  throw new Error(
444
444
  `pull request #${number} is ${String(details.state).toLowerCase()} — ` +
445
- "use --no-push for a read-only review",
445
+ "check out the PR locally and use jury review-once --dir <checkout>",
446
446
  );
447
447
  }
448
448
 
@@ -0,0 +1,13 @@
1
+ import path from "node:path";
2
+ import { serve } from "./server.js";
3
+ import { appendEvent, readEvents, foldEvents, writeRun } from "./store.js";
4
+
5
+ /** Publish the selected run before the server can answer the browser's first request. */
6
+ export async function startReviewConsole({ dir, target, cwd, port, onLog }, serveConsole = serve) {
7
+ await appendEvent(dir, { t: "target", target: { ...target, stateNote: "starting review" } });
8
+ await writeRun(dir, foldEvents(await readEvents(dir), { target }));
9
+ const { url } = await serveConsole({ port, cwd, onLog });
10
+ const currentRunUrl = new URL(url);
11
+ currentRunUrl.searchParams.set("run", path.basename(dir));
12
+ return currentRunUrl.href;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsdance/codejury",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Independent AI reviewers that iterate until your pull request is clean.",
5
5
  "keywords": [
6
6
  "code-review",
package/web/index.html CHANGED
@@ -485,7 +485,7 @@
485
485
 
486
486
  <section id="convo-sec">
487
487
  <h2>The conversation</h2>
488
- <p class="note" id="c-note">Claude on the left, the reviewer on the right. Each reviewer is a
488
+ <p class="note" id="c-note">The judge on the left, the reviewer on the right. Each reviewer is a
489
489
  separate thread — they never see each other's findings, which is the only reason running more
490
490
  than one counts as evidence.</p>
491
491
  <div class="convo-bar">