@xsft/panels 0.3.2 → 0.3.3

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
@@ -31,7 +31,7 @@ Run the command in the folder of the product. Each judge starts in that folder,
31
31
 
32
32
  On a terminal, `panels run` shows the score table, and each judge runs in its interactive harness in a pseudo-terminal inside the command. Each judge writes `judges/N/answer.json` and stays open. The table writes the report when each judge has an answer or an error, and the command exits from `report.json` when you quit. Without a terminal, the judges run in the background (`claude -p`, `codex exec`).
33
33
 
34
- Keys in the table: `1` to `9` open the screen of that judge, and `q` quits and asks `y` while a judge still runs. In a judge, each key goes to the judge, and `ctrl-]` returns to the table.
34
+ Keys in the table: `1` to `9` open the screen of that judge, and `q` quits and asks `y` while a judge still runs. In a judge, each key goes to the judge, and `ctrl-q` returns to the table. A harness that has not seen the folder asks whether to trust it. The first judge starts alone; press `1`, answer, and the other judges start.
35
35
 
36
36
  ## Run folder
37
37
 
package/lib/table.js CHANGED
@@ -8,19 +8,23 @@ import xterm from '@xterm/headless';
8
8
  import { check, MINUTES_PER_CRITERION, parseAnswer, writeJudge } from './judge.js';
9
9
  import { buildReport, renderMarkdown } from './report.js';
10
10
 
11
- // More parallel sessions on one account get 429 answers from the model api.
12
- const LIVE_JUDGES = 4;
11
+ const LIVE_JUDGES = 10;
12
+ // A harness asks this in a folder it has not seen. Only the first judge starts until its screen no longer asks,
13
+ // so the engineer answers once and the harness saves the answer for the other judges.
14
+ const TRUST_PROMPT = /trust this folder/i;
15
+ const FIRST_JUDGE_HEAD_START = 3000;
13
16
  const ORANGE = '\x1b[38;2;255;106;61m';
14
17
  const GREY = '\x1b[90m';
15
18
  const BOLD = '\x1b[1m';
16
19
  const RESET = '\x1b[0m';
17
- // ctrl-] returns from a judge to the table. A terminal in the kitty keyboard mode sends it as CSI 93;5u.
18
- const BACK = ['\x1d', '\x1b[93;5u'];
20
+ // ctrl-q returns from a judge to the table. A terminal in the kitty keyboard mode sends it as CSI 113;5u.
21
+ const BACK = ['\x11', '\x1b[113;5u'];
19
22
  // Turns off each mode that the screen of a judge can turn on, so the table gets the keys and the screen as before.
20
23
  const TABLE_MODES = '\x1b[?1049h\x1b[r\x1b[0m\x1b[4l\x1b[?6l\x1b[?7h\x1b[?1l\x1b>\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1004l\x1b[?2004l\x1b[<u\x1b[?25l\x1b[2J';
21
24
 
22
25
  /**
23
- * The lines of the table. rows hold { rank, name, state, result }, and state is queued, running, done, or error.
26
+ * The lines of the table. rows hold { rank, name, state, result, trust }, and state is queued, running, done, or error.
27
+ * trust is true while the harness of a running judge asks whether to trust the folder.
24
28
  * final is null until the report exists, then { status, path }. confirm is true while q waits for y.
25
29
  */
26
30
  const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -31,7 +35,8 @@ export function render({ title, rows, final, confirm, width, frame = 0 }) {
31
35
  const lines = [` ${BOLD}panels run${RESET} ${title}`, rule];
32
36
  for (const r of rows) {
33
37
  const head = `${String(r.rank).padStart(3)} ${r.name.padEnd(pad)}`;
34
- if (r.state === 'running') lines.push(`${ORANGE}${head}${SPIN[frame % SPIN.length]}${RESET}`);
38
+ if (r.state === 'running' && r.trust) lines.push(`${ORANGE}${head}press ${r.rank} and answer the trust prompt${RESET}`);
39
+ else if (r.state === 'running') lines.push(`${ORANGE}${head}${SPIN[frame % SPIN.length]}${RESET}`);
35
40
  else if (r.state === 'queued') lines.push(`${head}${GREY}queued${RESET}`);
36
41
  else if (r.state === 'error') lines.push(`${head}error`);
37
42
  else lines.push(`${head}${r.result.score} ${GREY}b${r.result.band}${RESET}`);
@@ -80,7 +85,14 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
80
85
  }
81
86
  if (judges[i] === undefined) return { state: 'queued' };
82
87
  if (Date.now() - judges[i].started > MINUTES_PER_CRITERION * 60_000) return { state: 'error', error: `no answer.json after ${MINUTES_PER_CRITERION} minutes` };
83
- return { state: 'running' };
88
+ return { state: 'running', trust: asksTrust(judges[i]) };
89
+ }
90
+
91
+ function asksTrust(j) {
92
+ const buffer = j.term.buffer.active;
93
+ const lines = [];
94
+ for (let y = 0; y < buffer.length; y++) lines.push(buffer.getLine(y).translateToString(true));
95
+ return TRUST_PROMPT.test(lines.join(' '));
84
96
  }
85
97
 
86
98
  function start(rank) {
@@ -113,7 +125,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
113
125
  function drawFooter() {
114
126
  if (!ui.view) return;
115
127
  const { cursorX, cursorY } = judges[ui.view - 1].term.buffer.active;
116
- out.write(`\x1b[${out.rows};1H${RESET}${GREY} judge ${ui.view} ctrl-] scores${RESET}\x1b[K\x1b[${cursorY + 1};${cursorX + 1}H`);
128
+ out.write(`\x1b[${out.rows};1H${RESET}${GREY} judge ${ui.view} ctrl-q scores${RESET}\x1b[K\x1b[${cursorY + 1};${cursorX + 1}H`);
117
129
  }
118
130
 
119
131
  // Waits for a pause in the output, so the footer does not land inside an escape sequence that a chunk split.
@@ -153,7 +165,10 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
153
165
  if (!final) {
154
166
  rows = criteria.map((c, i) => ({ ...c, ...state(c, i) }));
155
167
  const live = rows.filter((r) => r.state === 'running').length;
156
- for (const r of rows.filter((x) => x.state === 'queued').slice(0, LIVE_JUDGES - live)) {
168
+ const first = judges[0];
169
+ const hold = first && !first.exited && (Date.now() - first.started < FIRST_JUDGE_HEAD_START || rows[0].trust);
170
+ for (const r of rows.filter((x) => x.state === 'queued').slice(0, first ? LIVE_JUDGES - live : 1)) {
171
+ if (hold) break;
157
172
  start(r.rank);
158
173
  r.state = 'running';
159
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsft/panels",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "Run a panel of AI judges from cross.software/panels against a target.",
6
6
  "type": "module",