@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/README.md +32 -5
- package/bin/jury.js +163 -83
- package/lib/config.js +3 -2
- package/lib/directories.js +27 -0
- package/lib/findings.js +5 -1
- package/lib/loop.js +24 -10
- package/lib/repository.js +326 -9
- package/lib/store.js +5 -1
- package/package.json +1 -1
- package/web/index.html +55 -50
package/web/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<title>Code Jury —
|
|
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
|
-
|
|
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
|
-
/*
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
.convo-grid[data-stack="1"] .msgs {
|
|
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
|
-
/*
|
|
441
|
-
|
|
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
|
-
|
|
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:"
|
|
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)}
|
|
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
|
|
850
|
-
|
|
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 "
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
1159
|
-
//
|
|
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
|
-
*
|
|
1166
|
-
*
|
|
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}>${
|
|
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 ===
|
|
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(
|
|
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
|
|
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. ${
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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 ===
|
|
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:
|
|
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:
|
|
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
|
}
|