@agentsdance/codejury 0.1.0 → 0.1.2

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/web/index.html CHANGED
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <title>Code Jury — Convergence Console</title>
2
+ <title>Code Jury — Review Console</title>
3
3
  <style>
4
4
  :root {
5
5
  --ground:#F4F6F8; --surface:#FFFFFF; --surface-alt:#EDF0F4;
@@ -237,7 +237,7 @@
237
237
  .io { background:var(--t-bg); border:1px solid var(--t-line); border-radius:6px;
238
238
  overflow:hidden; box-shadow:var(--shadow); font-family:var(--mono); }
239
239
  .io-body { padding:13px 16px 18px; font-size:12.6px; line-height:1.6; color:var(--t-fg);
240
- max-height:640px; overflow:auto; }
240
+ overflow-x:auto; }
241
241
 
242
242
  /* One column per reviewer. They are side by side to make the independence
243
243
  legible at a glance; below the breakpoint they stack, which is the same
@@ -258,7 +258,6 @@
258
258
  .convo-switch .pr-chip[aria-pressed="true"]:hover { color:#fff; }
259
259
  .convo-switch .pr-chip[aria-pressed="false"] { opacity:.7; }
260
260
  .convo-note { margin:0 0 10px; font-size:12.5px; color:var(--muted); }
261
- .convo .io-body { max-height:none; }
262
261
  /* A long unbroken report must scroll inside its own column rather than
263
262
  widening the grid and pushing the other conversation off-screen. */
264
263
  .convo .stream { white-space:pre-wrap; overflow-wrap:anywhere; }
@@ -362,12 +361,12 @@
362
361
  .convo > header .d { width:8px; height:8px; border-radius:50%; background:var(--ok); flex:none; }
363
362
  .convo > header .d[data-live="1"] { background:var(--warn); animation:blip 1.4s ease-in-out infinite; }
364
363
  @keyframes blip { 0%,100%{opacity:1} 50%{opacity:.25} }
365
- /* The thread is what this page exists to show, so it gets real height: tall
366
- enough to read in, with a floor so a short conversation still reads as the
367
- main surface rather than a strip under the timeline. */
364
+ /* Completed threads grow with their content. The page owns vertical
365
+ scrolling; neither the thread nor an individual message hides findings in
366
+ a nested scroll region. */
368
367
  .msgs { padding:14px 16px; display:flex; flex-direction:column; gap:10px;
369
- overflow-y:auto; max-height:82vh; min-height:min(34em, 58vh); }
370
- .convo-grid[data-stack="1"] .msgs { max-height:56vh; min-height:0; }
368
+ min-height:min(34em, 58vh); }
369
+ .convo-grid[data-stack="1"] .msgs { min-height:0; }
371
370
  /* Wider than before: bubbles now carry the reviewer's report in full, and a
372
371
  narrow column turns a report into a ribbon. */
373
372
  .msg { max-width:min(88%, 940px); display:flex; flex-direction:column; gap:4px; min-width:0; }
@@ -437,9 +436,8 @@
437
436
  .bub[data-k="streaming"]::after {
438
437
  content:"▍"; animation:blip 1s steps(2) infinite; color:var(--accent);
439
438
  }
440
- /* A long message keeps all of its text and gives up height instead. The
441
- inset shadows are the affordance: they say "there is more above/below"
442
- without a control to click. */
439
+ /* Only a live transcript is compact. Once the report completes, it is
440
+ rendered directly in the conversation at full height. */
443
441
  .bub .tall { max-height:26em; overflow-y:auto; overscroll-behavior:contain;
444
442
  scrollbar-width:thin; border-radius:3px;
445
443
  background:
@@ -522,7 +520,8 @@ let RUN = {
522
520
  url: "https://github.com/acme/worker-pool/pull/128",
523
521
  title: "Grow the CreateSandbox retry wait on repeated failures",
524
522
  branch: "fix/sandbox-manager-create-backoff", trunk: "master",
525
- diffstat: "2 files · +418 −13", state: "merged", stateNote: "merged · converged on ae473ab",
523
+ judge: "claude",
524
+ diffstat: "2 files · +418 −13", state: "merged", stateNote: "Merged", reviewedCommit: "ae473ab",
526
525
  summary: "Two reviewers — codex and droid — read each pushed commit independently. The main agent triages what they find, fixes what survives verification, answers every finding including the ones it rejects, and pushes again. Three rounds until both reported nothing new."
527
526
  },
528
527
  totalMin: 100,
@@ -594,7 +593,7 @@ let RUN = {
594
593
  { who:"codex", ts:"round 1 · T+51m", body:"Accepted. The producer reservation code is byte-for-byte outside this commit's diff, so the issue is pre-existing. The new backoff does not materially widen the concurrent-producer window; cancellation-aware worker exit arguably reduces old/new manager overlap. I agree it belongs in a separate PR." } ] }
595
594
  ],
596
595
  ship: [
597
- { n:"3", k:"rounds", s:"to convergence" },
596
+ { n:"3", k:"rounds", s:"until complete" },
598
597
  { n:"5", k:"exchanges", s:"3 fixed · 1 deferred · 1 rejected" },
599
598
  { n:"11", k:"turns", s:"every finding answered" },
600
599
  { n:"0", k:"prod bugs", s:"found in these 3 rounds" },
@@ -760,7 +759,7 @@ function draw(){
760
759
  ${(x.turns ?? []).map(tt => `
761
760
  <div class="ln" data-who="${tt.who}">
762
761
  <span class="at">${when(tt)}</span>
763
- <span class="nm">${icon(tt.who)}${esc(tt.who)}</span>
762
+ <span class="nm"${tt.who === judgeName() ? ' style="color:var(--t-claude)"' : ""}>${icon(tt.who)}${esc(tt.who)}</span>
764
763
  <span class="say">${rich(tt.body)}
765
764
  ${tt.quote ? `<div class="q">${esc(tt.quote)}</div>` : ""}
766
765
  ${tt.ask ? `<div class="ask">${esc(tt.ask)}</div>` : ""}
@@ -796,7 +795,7 @@ function draw(){
796
795
  return `
797
796
  <div class="xgroup">
798
797
  <div class="rule" data-who="${esc(agent)}">
799
- <span class="lbl">${icon(agent)}claude ⇄ ${esc(agent)}</span><i></i>
798
+ <span class="lbl">${icon(agent)}${esc(judgeName())} ⇄ ${esc(agent)}</span><i></i>
800
799
  <span class="rt">${mine.length} of ${shown} finding(s)</span>
801
800
  </div>
802
801
  ${mine.map(card).join("")}
@@ -845,9 +844,11 @@ function renderHeader(){
845
844
  const branch = t.branch ? `<span>${esc(t.branch)} → ${esc(t.trunk ?? "master")}</span>` : "";
846
845
  const stat = t.diffstat ? `<span>${esc(t.diffstat)}</span>` : "";
847
846
  const cls = BADGE[t.state] ?? "badge-mute";
847
+ const commit = reviewedCommit(t);
848
848
  const badge = t.state
849
- ? `<span class="${cls}"><span class="d"></span>${esc(t.stateNote || t.state)}</span>` : "";
850
- $("t-meta").innerHTML = [link, branch, stat, badge].filter(Boolean).join("");
849
+ ? `<span class="${cls}"><span class="d"></span>${esc(statusLabel(t))}</span>` : "";
850
+ const reviewed = commit ? `<span>Reviewed commit <code>${esc(commit)}</code></span>` : "";
851
+ $("t-meta").innerHTML = [link, branch, stat, badge, reviewed].filter(Boolean).join("");
851
852
 
852
853
  // Only worth showing when there is something to switch between.
853
854
  //
@@ -934,7 +935,7 @@ function runSummary(r) {
934
935
  const t = r.target ?? {};
935
936
  if (isLive(r) && t.stateNote) return t.stateNote;
936
937
  const n = (r.exchanges ?? []).length;
937
- if (t.state === "converged") return "converged";
938
+ if (t.state === "converged") return "Review complete";
938
939
  const found = n ? `${n} finding${n === 1 ? "" : "s"}` : "";
939
940
  // A stale "review" is a run that stopped without finishing. Saying "review"
940
941
  // would repeat the claim that misled the grouping in the first place.
@@ -942,6 +943,17 @@ function runSummary(r) {
942
943
  return found || t.stateNote || t.state || "";
943
944
  }
944
945
 
946
+ function reviewedCommit(t) {
947
+ if (t?.reviewedCommit) return t.reviewedCommit;
948
+ return (t?.stateNote?.match(/\bconverged on ([0-9a-f]{7,40})\b/i)?.[1] ?? "").toLowerCase();
949
+ }
950
+
951
+ function statusLabel(t) {
952
+ if (t?.state === "converged") return "Review complete";
953
+ if (t?.state === "merged" && /converged/i.test(t.stateNote ?? "")) return "Merged";
954
+ return t?.stateNote || t?.state || "";
955
+ }
956
+
945
957
  function drawSwitcher() {
946
958
  const el = $("switcher");
947
959
  if (!el) return;
@@ -1125,14 +1137,15 @@ function render(){
1125
1137
  folded run; a conversation needs the events in the order they were spoken,
1126
1138
  and folding throws that order away.
1127
1139
 
1128
- Side is fixed by speaker, never by turn order: claude left, reviewer right.
1140
+ Side is fixed by speaker, never by turn order: judge left, reviewer right.
1129
1141
  A reader should be able to tell who is talking from across the room. */
1130
1142
  let THREADS = []; // [{agent, turns:[…]}]
1131
1143
  let CONVO_STACK = false; // one thread full width, or every thread stacked
1132
1144
  let CONVO_PICK = null; // which thread, when not stacked
1133
1145
  const CONVO_SHUT = new Set(); // collapsed threads, by agent, while stacked
1134
1146
 
1135
- const SIDE = (who) => (who === "claude" ? "l" : "r");
1147
+ const judgeName = () => RUN.target?.judge ?? "claude";
1148
+ const SIDE = (who) => (who === judgeName() ? "l" : "r");
1136
1149
 
1137
1150
  // A stable colour per agent. codex and droid have tokens already; anything else
1138
1151
  // is hashed to a hue so a five-reviewer run does not render five identical
@@ -1155,22 +1168,12 @@ const VERDICT_TEXT = {
1155
1168
  superseded: "Superseded",
1156
1169
  };
1157
1170
 
1158
- // Long reports collapse. A twenty-minute reviewer turn can be thousands of
1159
- // lines, and an un-collapsed one buries every exchange after it.
1171
+ // A live transcript can be thousands of lines before it becomes a completed
1172
+ // report. Keep only that in-progress state compact; completed messages expand.
1160
1173
  const CLAMP = 700;
1161
- /* The collapsing half of a bubble, on its own so that bubbles which lead with a
1162
- header — a verdict, a named claim — can clamp their body without clamping the
1163
- header along with it. */
1164
1174
  /**
1165
- * Every message in full, always. Nothing is truncated and nothing hides behind
1166
- * a button: reading a review should not mean clicking through it.
1167
- *
1168
- * The reason a clamp existed is still real — evidence arrives verbatim from
1169
- * `jury finding reproduce --evidence` and is routinely thousands of lines, and
1170
- * one such turn inlined at full height buries every turn after it. So a long
1171
- * message scrolls WITHIN ITS OWN BOX instead of being cut: the whole text is in
1172
- * the DOM, selectable and searchable with the browser's own find, while the
1173
- * thread below it stays reachable. Bounded height, not bounded content.
1175
+ * Compact an in-progress transcript without truncating its DOM content.
1176
+ * Completed reports bypass this helper and expand to their full height.
1174
1177
  */
1175
1178
  function clamped(text) {
1176
1179
  const t = text ?? "";
@@ -1179,7 +1182,7 @@ function clamped(text) {
1179
1182
  aria-label="long message, ${bytes(t.length)}, scrollable">${esc(t)}</div>`;
1180
1183
  }
1181
1184
  function bubble(text, k, attrs = "") {
1182
- return `<div class="bub" data-k="${k}" ${attrs}>${clamped(text)}</div>`;
1185
+ return `<div class="bub" data-k="${k}" ${attrs}>${esc(text ?? "")}</div>`;
1183
1186
  }
1184
1187
  /** m:ss, or h:mm:ss once a reviewer has been going that long. */
1185
1188
  function hms(sec) {
@@ -1235,19 +1238,17 @@ function stampFor(t) {
1235
1238
 
1236
1239
  function turnHTML(t, agent) {
1237
1240
  const side = SIDE(t.who);
1238
- const nm = t.who === "claude" ? "claude" : agent;
1241
+ const nm = t.who === judgeName() ? judgeName() : agent;
1239
1242
  let body = "";
1240
1243
  switch (t.kind) {
1241
1244
  case "finding":
1242
1245
  body = `<div class="bub" data-k="finding"><span class="hd">${esc(t.claim ?? "")}</span
1243
- >${t.loc ? `<span class="loc">${esc(t.loc)}</span>\n` : ""}${esc((t.body ?? "").slice(0, CLAMP))}</div>`;
1246
+ >${t.loc ? `<span class="loc">${esc(t.loc)}</span>\n` : ""}${esc(t.body ?? "")}</div>`;
1244
1247
  break;
1245
1248
  case "reproduced":
1246
- // Evidence is whatever the reviewer passed to `finding reproduce
1247
- // --evidence`, which is routinely a wall of captured command output. It
1248
- // collapses, or one reproduction buries every turn below it.
1249
+ // Evidence is shown inline in full once reproduction has completed.
1249
1250
  body = `<div class="bub" data-k="reproduced"
1250
- >${t.claim ? `<span class="re">${esc(t.claim)}</span>\n` : ""}Reproduced. ${clamped(t.text ?? "")}</div>`;
1251
+ >${t.claim ? `<span class="re">${esc(t.claim)}</span>\n` : ""}Reproduced. ${esc(t.text ?? "")}</div>`;
1251
1252
  break;
1252
1253
  case "verdict":
1253
1254
  // The claim is named here because the findings are no longer bubbles of
@@ -1262,7 +1263,7 @@ function turnHTML(t, agent) {
1262
1263
  // being re-argued, so it names the claim and leaves the words to the
1263
1264
  // report. Claude's turn in the thread does carry text, and shows it.
1264
1265
  body = `<div class="bub" data-k="rebuttal"
1265
- ><span class="hd">${t.who === "claude" ? "still open" : "not convinced"}</span
1266
+ ><span class="hd">${t.who === judgeName() ? "still open" : "not convinced"}</span
1266
1267
  >${t.claim ? `<span class="re">${esc(t.claim)}</span>` : ""}${t.text ? `\n${esc(t.text)}` : ""}</div>`;
1267
1268
  break;
1268
1269
  case "commit":
@@ -1299,7 +1300,7 @@ function turnHTML(t, agent) {
1299
1300
  body = bubble(t.text ?? "", esc(t.kind ?? "turn"));
1300
1301
  }
1301
1302
  return `<div class="msg" data-side="${side}"><span class="nm"
1302
- ><span class="ic" style="color:${nm === "claude" ? "var(--main-hue)" : hueFor(nm)}">${icon(nm)}</span>${esc(nm)}${stampFor(t)}</span>${body}</div>`;
1303
+ ><span class="ic" style="color:${nm === judgeName() ? "var(--main-hue)" : hueFor(nm)}">${icon(nm)}</span>${esc(nm)}${stampFor(t)}</span>${body}</div>`;
1303
1304
  }
1304
1305
 
1305
1306
  /**
@@ -1380,7 +1381,7 @@ function drawConvo() {
1380
1381
  return `<div class="convo" data-a="${esc(t.agent)}" data-open="${open ? 1 : 0}">
1381
1382
  <header data-fold-agent="${esc(t.agent)}"><span class="tw">${open ? "▾" : "▸"}</span
1382
1383
  ><span class="d" data-live="${l ? 1 : 0}"></span>
1383
- <span class="who"><span class="ic" style="color:var(--main-hue)">${icon("claude")}</span>claude
1384
+ <span class="who"><span class="ic" style="color:var(--main-hue)">${icon(judgeName())}</span>${esc(judgeName())}
1384
1385
  <span class="sw">⇄</span>
1385
1386
  <span class="ic" style="color:${hueFor(t.agent)}">${icon(t.agent)}</span>${esc(t.agent)}</span>
1386
1387
  <span class="st">${t.turns.length} turn${t.turns.length === 1 ? "" : "s"}${l ? " · live" : ""}</span>
@@ -1502,8 +1503,12 @@ function foldConversation(events) {
1502
1503
  };
1503
1504
  const owner = new Map();
1504
1505
  const claimOf = new Map();
1506
+ let judge = "claude";
1505
1507
  for (const e of events) {
1506
1508
  switch (e.t) {
1509
+ case "target":
1510
+ judge = e.target?.judge ?? judge;
1511
+ break;
1507
1512
  case "finding.raised":
1508
1513
  owner.set(e.id, e.agent);
1509
1514
  // Deliberately NOT a turn of its own. The reviewer already said this in
@@ -1518,12 +1523,12 @@ function foldConversation(events) {
1518
1523
  thread(e.agent);
1519
1524
  break;
1520
1525
  case "finding.reproduced":
1521
- thread(owner.get(e.id) ?? "?").turns.push({ who: "claude", kind: "reproduced",
1526
+ thread(owner.get(e.id) ?? "?").turns.push({ who: e.who ?? judge, kind: "reproduced",
1522
1527
  id: e.id, claim: claimOf.get(e.id) ?? "",
1523
1528
  text: typeof e.evidence === "string" ? e.evidence : "", ts: e.ts });
1524
1529
  break;
1525
1530
  case "finding.resolved":
1526
- thread(owner.get(e.id) ?? "?").turns.push({ who: "claude", kind: "verdict",
1531
+ thread(owner.get(e.id) ?? "?").turns.push({ who: e.who ?? judge, kind: "verdict",
1527
1532
  id: e.id, claim: claimOf.get(e.id) ?? "",
1528
1533
  verdict: e.verdict, text: e.reason ?? "", test: e.test ?? "", ts: e.ts });
1529
1534
  break;
@@ -1532,12 +1537,12 @@ function foldConversation(events) {
1532
1537
  // and that reply is already rendered whole as its `answer` turn — so the
1533
1538
  // text is deliberately dropped, exactly as a raised finding's is. What
1534
1539
  // survives is the linkage: which claim is being re-argued, and by whom.
1535
- // Claude's own turn is the exception: it is not an excerpt of anything
1540
+ // The judge's own turn is the exception: it is not an excerpt of anything
1536
1541
  // shown elsewhere, so dropping its text would lose it entirely.
1537
1542
  const who = e.who ?? "?";
1538
1543
  const turn = { who, kind: "rebuttal", id: e.id,
1539
1544
  claim: claimOf.get(e.id) ?? "", ts: e.ts };
1540
- if (who === "claude") turn.text = e.text ?? "";
1545
+ if (who === judge) turn.text = e.text ?? "";
1541
1546
  thread(owner.get(e.id) ?? e.agent ?? "?").turns.push(turn);
1542
1547
  break;
1543
1548
  }
@@ -1589,7 +1594,7 @@ function foldConversation(events) {
1589
1594
  break;
1590
1595
  }
1591
1596
  case "reply.sent":
1592
- thread(e.agent).turns.push({ who: "claude", kind: "reply", text: e.text ?? "", ts: e.ts });
1597
+ thread(e.agent).turns.push({ who: e.who ?? judge, kind: "reply", text: e.text ?? "", ts: e.ts });
1593
1598
  break;
1594
1599
  case "reply.answered":
1595
1600
  thread(e.agent).turns.push({ who: e.agent, kind: "answer",
@@ -1597,7 +1602,7 @@ function foldConversation(events) {
1597
1602
  break;
1598
1603
  case "commit.pushed":
1599
1604
  for (const t of threads.values()) {
1600
- t.turns.push({ who: "claude", kind: "commit", sha: e.sha, text: e.subject ?? "", ts: e.ts });
1605
+ t.turns.push({ who: e.who ?? judge, kind: "commit", sha: e.sha, text: e.subject ?? "", ts: e.ts });
1601
1606
  }
1602
1607
  break;
1603
1608
  }