@xsft/panels 0.2.0 → 0.3.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 +4 -5
- package/bin/panels.js +6 -15
- package/lib/judge.js +16 -21
- package/lib/table.js +131 -42
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -27,11 +27,11 @@ npx @xsft/panels run xsft/marketing-site/v6 . at http://localhost:4321 --sources
|
|
|
27
27
|
|
|
28
28
|
Run the command in the folder of the product. Each judge starts in that folder, so `.`, a branch, and relative paths resolve there. The CLI does not test, fetch, or change the target. Four judges run at a time. A judge gets 20 minutes.
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Terminal mode
|
|
31
31
|
|
|
32
|
-
On a terminal
|
|
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
|
|
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.
|
|
35
35
|
|
|
36
36
|
## Run folder
|
|
37
37
|
|
|
@@ -39,8 +39,7 @@ Each run writes to `.panels/<owner>-<name>/<UTC time>/`:
|
|
|
39
39
|
|
|
40
40
|
- `panel.json`: the panel as the api gave it.
|
|
41
41
|
- `report.md` and `report.json`: the scores, the evidence, the fixes, and the errors. `schema/report.json` is the schema of `report.json`.
|
|
42
|
-
- `judges/<k>/`: the prompt, the output schema, and the raw answer of judge k. In
|
|
43
|
-
- `run.json`, in tmux mode: the session name, the harness, the folder, and the target, for window 0.
|
|
42
|
+
- `judges/<k>/`: the prompt, the output schema, and the raw answer of judge k. In terminal mode, `answer.json` takes the place of the raw answer.
|
|
44
43
|
|
|
45
44
|
The last line on stdout is the path of `report.json`.
|
|
46
45
|
|
package/bin/panels.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
3
|
import { delimiter, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
4
|
import { parse, SYNOPSIS } from '../lib/args.js';
|
|
7
|
-
import { runJudge
|
|
5
|
+
import { runJudge } from '../lib/judge.js';
|
|
8
6
|
import { fetchPanel } from '../lib/panel.js';
|
|
9
7
|
import { buildReport, renderMarkdown } from '../lib/report.js';
|
|
8
|
+
import { runTable } from '../lib/table.js';
|
|
10
9
|
|
|
11
10
|
// More parallel sessions on one account get 429 answers from the model api.
|
|
12
11
|
const PARALLEL_JUDGES = 4;
|
|
@@ -28,17 +27,9 @@ async function pool(items, size, fn) {
|
|
|
28
27
|
|
|
29
28
|
const EXIT = { pass: 0, fail: 1, error: 2 };
|
|
30
29
|
|
|
31
|
-
/**
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
writeFileSync(join(runDir, 'run.json'), JSON.stringify({ session, harness, cwd, target: args.target }, null, 2));
|
|
35
|
-
panel.criteria.forEach((c, i) => writeJudge({ dir: join(runDir, 'judges', String(i + 1)), criteria: [{ rank: i + 1, ...c }], harness, cwd, target: args.target, brief: args.brief, sources: args.sources }));
|
|
36
|
-
execFileSync('tmux', ['new-session', '-d', '-s', session, '-n', 'scores', process.execPath, fileURLToPath(new URL('../lib/table.js', import.meta.url)), runDir]);
|
|
37
|
-
if (process.env.TMUX) {
|
|
38
|
-
execFileSync('tmux', ['switch-client', '-t', session]);
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
spawnSync('tmux', ['attach-session', '-t', session], { stdio: 'inherit' });
|
|
30
|
+
/** Runs the judges in the score table, and exits from report.json when the engineer quits. */
|
|
31
|
+
async function runOnTerminal({ panel, runDir, harness, cwd, args }) {
|
|
32
|
+
await runTable({ panel, runDir, harness, cwd, target: args.target, brief: args.brief, sources: args.sources });
|
|
42
33
|
const reportPath = join(runDir, 'report.json');
|
|
43
34
|
if (!existsSync(reportPath)) return 2;
|
|
44
35
|
console.log(reportPath);
|
|
@@ -76,7 +67,7 @@ async function main() {
|
|
|
76
67
|
const runDir = join(cwd, '.panels', `${panel.owner}-${panel.name}`, time);
|
|
77
68
|
mkdirSync(runDir, { recursive: true });
|
|
78
69
|
writeFileSync(join(runDir, 'panel.json'), JSON.stringify(panel, null, 2));
|
|
79
|
-
if (process.stdout.isTTY && process.stdin.isTTY
|
|
70
|
+
if (process.stdout.isTTY && process.stdin.isTTY) return runOnTerminal({ panel, runDir, harness, cwd, args });
|
|
80
71
|
const outcomes = await pool(judges, PARALLEL_JUDGES, (indexes, j) => runJudge({
|
|
81
72
|
dir: join(runDir, 'judges', String(j + 1)),
|
|
82
73
|
criteria: indexes.map((i) => ({ rank: i + 1, ...panel.criteria[i] })),
|
package/lib/judge.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
4
|
|
|
6
5
|
const { judgeResult } = JSON.parse(readFileSync(new URL('../schema/report.json', import.meta.url), 'utf8')).$defs;
|
|
7
|
-
const
|
|
8
|
-
const SYSTEM_PROMPT = readFileSync(SYSTEM_PROMPT_FILE, 'utf8');
|
|
6
|
+
const SYSTEM_PROMPT = readFileSync(new URL('../prompts/judge-system-prompt.txt', import.meta.url), 'utf8');
|
|
9
7
|
export const MINUTES_PER_CRITERION = 20;
|
|
10
8
|
const TOOLS = 'Read,Glob,Grep,Bash,WebFetch,WebSearch';
|
|
11
9
|
// With --permission-mode dontAsk, Claude refuses every tool call that this list does not allow, so Bash runs git reads only.
|
|
@@ -146,31 +144,28 @@ export async function runJudge({ dir, criteria, harness, cwd, target, brief, sou
|
|
|
146
144
|
return errors.length ? { error: `invalid answer after one re-ask: ${errors.join(' ')}` } : { results: reply.answer.results };
|
|
147
145
|
}
|
|
148
146
|
|
|
149
|
-
/** Quotes a string for sh. */
|
|
150
|
-
export const sq = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
151
|
-
|
|
152
147
|
const TOML_ESCAPES = { '"': '\\"', '\\': '\\\\', '\n': '\\n' };
|
|
153
148
|
|
|
154
149
|
/** A TOML basic string, for the value of a codex -c flag. */
|
|
155
150
|
export const tomlString = (s) => `"${s.replace(/["\\\x00-\x1f\x7f]/g, (c) => TOML_ESCAPES[c] ?? `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`)}"`;
|
|
156
151
|
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Writes prompt.md and schema.json of one judge of the terminal mode, and returns the command and the
|
|
154
|
+
* arguments that start the judge in its interactive harness.
|
|
155
|
+
*/
|
|
156
|
+
export function writeJudge({ dir, criteria, harness, cwd, target, brief, sources }) {
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
const closing = `The engineer ran the command in ${cwd}. As your last step, write your answer JSON, which matches the schema in ${join(dir, 'schema.json')}, to ${join(dir, 'answer.json')}. Write that file yourself with a tool, and do not start sub-agents: the JSON in your reply does not count. Then stop. These two files are the only exceptions to the rules on writing files and on the .panels folder.\n`;
|
|
159
|
+
const prompt = `${userMessage(criteria, { target, brief, sources })}\n${closing}`;
|
|
160
|
+
writeFileSync(join(dir, 'prompt.md'), prompt);
|
|
161
|
+
writeFileSync(join(dir, 'schema.json'), JSON.stringify(judgeSchema(criteria), null, 2));
|
|
160
162
|
if (harness === 'codex') {
|
|
161
|
-
|
|
163
|
+
// Codex ranks the developer instructions above the prompt, and the system prompt says to return the JSON and to
|
|
164
|
+
// write no files. Without the closing in the developer instructions, the judge prints the JSON and writes no answer.json.
|
|
165
|
+
return ['codex', '-C', dir, '-s', 'workspace-write', '--disable', 'multi_agent', '--disable', 'apps', '-c', `developer_instructions=${tomlString(`${SYSTEM_PROMPT}\n${closing}`)}`, prompt];
|
|
162
166
|
}
|
|
163
|
-
const add = addDirs(cwd, brief, sources).map((d) => ` --add-dir ${sq(d)}`).join('');
|
|
164
167
|
// Claude matches file paths only in Edit rules, which cover Write too; // starts an absolute path.
|
|
165
168
|
const allowed = `${ALLOWED_TOOLS},Edit(/${join(dir, 'answer.json')})`;
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Writes prompt.md, schema.json, and start.sh of one judge of the tmux mode. */
|
|
170
|
-
export function writeJudge({ dir, criteria, harness, cwd, target, brief, sources }) {
|
|
171
|
-
mkdirSync(dir, { recursive: true });
|
|
172
|
-
const closing = `The engineer ran the command in ${cwd}. As your last step, write your answer JSON, which matches the schema in ${join(dir, 'schema.json')}, to ${join(dir, 'answer.json')}. Then stop. These two files are the only exceptions to the rules on writing files and on the .panels folder.\n`;
|
|
173
|
-
writeFileSync(join(dir, 'prompt.md'), `${userMessage(criteria, { target, brief, sources })}\n${closing}`);
|
|
174
|
-
writeFileSync(join(dir, 'schema.json'), JSON.stringify(judgeSchema(criteria), null, 2));
|
|
175
|
-
writeFileSync(join(dir, 'start.sh'), startScript({ dir, harness, cwd, brief, sources }));
|
|
169
|
+
const add = addDirs(cwd, brief, sources).flatMap((d) => ['--add-dir', d]);
|
|
170
|
+
return ['claude', '--safe-mode', '--system-prompt', SYSTEM_PROMPT, '--tools', `${TOOLS},Write`, ...add, '--allowedTools', allowed, '--permission-mode', 'dontAsk', prompt];
|
|
176
171
|
}
|
package/lib/table.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// The terminal mode of `panels run`: the score table, and a view of each judge. Each judge runs its interactive
|
|
2
|
+
// harness in a pseudo-terminal, and a headless terminal keeps its screen for the switch to its view.
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import pty from '@homebridge/node-pty-prebuilt-multiarch';
|
|
6
|
+
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
7
|
+
import xterm from '@xterm/headless';
|
|
8
|
+
import { check, MINUTES_PER_CRITERION, parseAnswer, writeJudge } from './judge.js';
|
|
7
9
|
import { buildReport, renderMarkdown } from './report.js';
|
|
8
10
|
|
|
9
11
|
// More parallel sessions on one account get 429 answers from the model api.
|
|
@@ -12,18 +14,24 @@ const ORANGE = '\x1b[38;2;255;106;61m';
|
|
|
12
14
|
const GREY = '\x1b[90m';
|
|
13
15
|
const BOLD = '\x1b[1m';
|
|
14
16
|
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'];
|
|
19
|
+
// 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
|
+
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';
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* The lines of
|
|
23
|
+
* The lines of the table. rows hold { rank, name, state, result }, and state is queued, running, done, or error.
|
|
18
24
|
* final is null until the report exists, then { status, path }. confirm is true while q waits for y.
|
|
19
25
|
*/
|
|
20
|
-
|
|
26
|
+
const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
27
|
+
|
|
28
|
+
export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
21
29
|
const rule = `${GREY}${'─'.repeat(width)}${RESET}`;
|
|
22
30
|
const pad = Math.max(...rows.map((r) => r.name.length)) + 2;
|
|
23
31
|
const lines = [` ${BOLD}panels run${RESET} ${title}`, rule];
|
|
24
32
|
for (const r of rows) {
|
|
25
33
|
const head = `${String(r.rank).padStart(3)} ${r.name.padEnd(pad)}`;
|
|
26
|
-
if (r.state === 'running') lines.push(`${ORANGE}${head}
|
|
34
|
+
if (r.state === 'running') lines.push(`${ORANGE}${head}${SPIN[frame % SPIN.length]}${RESET}`);
|
|
27
35
|
else if (r.state === 'queued') lines.push(`${head}${GREY}queued${RESET}`);
|
|
28
36
|
else if (r.state === 'error') lines.push(`${head}error`);
|
|
29
37
|
else lines.push(`${head}${r.result.score} ${GREY}b${r.result.band}${RESET}`);
|
|
@@ -37,17 +45,31 @@ export function render({ title, rows, final, confirm, width }) {
|
|
|
37
45
|
return lines;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The next { view, confirm } after one key, plus send: true when the key goes to the judge, or quit: true.
|
|
50
|
+
* view is 0 for the table, or N for judge N. open holds the ranks of the judges that have started.
|
|
51
|
+
*/
|
|
52
|
+
export function press({ view, confirm }, key, { open, running }) {
|
|
53
|
+
if (view) return BACK.includes(key) ? { view: 0, confirm: false } : { view, confirm, send: true };
|
|
54
|
+
if (confirm) return { view: 0, confirm: false, quit: key === 'y' };
|
|
55
|
+
if (key === 'q') return running ? { view: 0, confirm: true } : { view: 0, confirm: false, quit: true };
|
|
56
|
+
if (/^[1-9]$/.test(key) && open.includes(Number(key))) return { view: Number(key), confirm: false };
|
|
57
|
+
return { view: 0, confirm: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Runs the judges, shows the table, and writes the report. Resolves when the engineer quits. */
|
|
61
|
+
export function runTable({ panel, runDir, harness, cwd, target, brief, sources }) {
|
|
62
|
+
const out = process.stdout;
|
|
44
63
|
const criteria = panel.criteria.map((c, i) => ({ rank: i + 1, ...c }));
|
|
45
|
-
const
|
|
64
|
+
const judges = [];
|
|
46
65
|
let rows = [];
|
|
47
66
|
let final = null;
|
|
48
|
-
let
|
|
67
|
+
let ui = { view: 0, confirm: false };
|
|
68
|
+
let footerTimer;
|
|
49
69
|
|
|
50
70
|
const judgeDir = (rank) => join(runDir, 'judges', String(rank));
|
|
71
|
+
// A judge gets one line less than the terminal. The last line holds the footer.
|
|
72
|
+
const size = () => ({ cols: out.columns, rows: out.rows - 1 });
|
|
51
73
|
|
|
52
74
|
function state(c, i) {
|
|
53
75
|
const file = join(judgeDir(c.rank), 'answer.json');
|
|
@@ -56,14 +78,60 @@ function main(runDir) {
|
|
|
56
78
|
const errors = check(answer, [c]);
|
|
57
79
|
return errors.length ? { state: 'error', error: `invalid answer: ${errors.join(' ')}` } : { state: 'done', result: answer.results[0] };
|
|
58
80
|
}
|
|
59
|
-
if (
|
|
60
|
-
if (Date.now() -
|
|
81
|
+
if (judges[i] === undefined) return { state: 'queued' };
|
|
82
|
+
if (Date.now() - judges[i].started > MINUTES_PER_CRITERION * 60_000) return { state: 'error', error: `no answer.json after ${MINUTES_PER_CRITERION} minutes` };
|
|
61
83
|
return { state: 'running' };
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
function start(rank) {
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
const [command, ...argv] = writeJudge({ dir: judgeDir(rank), criteria: [criteria[rank - 1]], harness, cwd, target, brief, sources });
|
|
88
|
+
const { cols, rows: lines } = size();
|
|
89
|
+
const term = new xterm.Terminal({ cols, rows: lines, allowProposedApi: true });
|
|
90
|
+
const serializer = new SerializeAddon();
|
|
91
|
+
term.loadAddon(serializer);
|
|
92
|
+
const proc = pty.spawn(command, argv, { name: 'xterm-256color', cols, rows: lines, cwd, env: process.env });
|
|
93
|
+
const j = { started: Date.now(), term, serializer, proc, modes: { 25: 'h', 1006: 'l' }, exited: false };
|
|
94
|
+
// The write callback keeps the order of the live output and of the screen that showJudge writes.
|
|
95
|
+
proc.onData((data) => term.write(data, () => {
|
|
96
|
+
if (ui.view !== rank) return;
|
|
97
|
+
out.write(data);
|
|
98
|
+
footer();
|
|
99
|
+
}));
|
|
100
|
+
proc.onExit(() => { j.exited = true; });
|
|
101
|
+
// The headless terminal answers the queries of the harness, such as the cursor position.
|
|
102
|
+
term.onData((data) => proc.write(data));
|
|
103
|
+
// The serializer does not keep the cursor visibility (25) or the mouse encoding (1006), so each judge keeps them here.
|
|
104
|
+
for (const final of ['h', 'l']) {
|
|
105
|
+
term.parser.registerCsiHandler({ prefix: '?', final }, (params) => {
|
|
106
|
+
for (const p of params) if (p in j.modes) j.modes[p] = final;
|
|
107
|
+
return false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
judges[rank - 1] = j;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function drawFooter() {
|
|
114
|
+
if (!ui.view) return;
|
|
115
|
+
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`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Waits for a pause in the output, so the footer does not land inside an escape sequence that a chunk split.
|
|
120
|
+
function footer() {
|
|
121
|
+
clearTimeout(footerTimer);
|
|
122
|
+
footerTimer = setTimeout(drawFooter, 50);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function showJudge(rank) {
|
|
126
|
+
const j = judges[rank - 1];
|
|
127
|
+
j.term.write('', () => {
|
|
128
|
+
if (ui.view !== rank) return;
|
|
129
|
+
// The table already holds the alternate screen, so a judge on its alternate screen gets a clear after
|
|
130
|
+
// the normal screen, which the serializer writes first. The scroll region keeps the judge off the footer line.
|
|
131
|
+
const screen = j.serializer.serialize({ scrollback: 0 }).replace('\x1b[?1049h\x1b[H', '$&\x1b[2J');
|
|
132
|
+
out.write(`${RESET}\x1b[H\x1b[2J\x1b[1;${out.rows - 1}r${screen}${Object.entries(j.modes).map(([mode, final]) => `\x1b[?${mode}${final}`).join('')}`);
|
|
133
|
+
drawFooter();
|
|
134
|
+
});
|
|
67
135
|
}
|
|
68
136
|
|
|
69
137
|
function finish() {
|
|
@@ -75,9 +143,10 @@ function main(runDir) {
|
|
|
75
143
|
final = { status: report.status, path: relative(cwd, reportPath) };
|
|
76
144
|
}
|
|
77
145
|
|
|
146
|
+
let frame = 0;
|
|
78
147
|
function draw() {
|
|
79
|
-
const lines = render({ title: `${panel.owner}/${panel.name} v${panel.version}`, rows, final, confirm, width: Math.min(
|
|
80
|
-
|
|
148
|
+
const lines = render({ title: `${panel.owner}/${panel.name} v${panel.version}`, rows, final, confirm: ui.confirm, width: Math.min(out.columns, 100), frame });
|
|
149
|
+
out.write(`\x1b[H${lines.map((l) => `${l}\x1b[K`).join('\n')}\x1b[J`);
|
|
81
150
|
}
|
|
82
151
|
|
|
83
152
|
function tick() {
|
|
@@ -90,30 +159,50 @@ function main(runDir) {
|
|
|
90
159
|
}
|
|
91
160
|
if (rows.every((r) => r.state === 'done' || r.state === 'error')) finish();
|
|
92
161
|
}
|
|
93
|
-
draw();
|
|
162
|
+
if (!ui.view) draw();
|
|
94
163
|
}
|
|
95
164
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
165
|
+
return new Promise((done) => {
|
|
166
|
+
const timer = setInterval(tick, 1000);
|
|
167
|
+
const spinner = setInterval(() => {
|
|
168
|
+
frame += 1;
|
|
169
|
+
if (!ui.view && !final) draw();
|
|
170
|
+
}, 100);
|
|
171
|
+
out.write('\x1b[?1049h\x1b[?25l');
|
|
172
|
+
process.stdin.setRawMode(true);
|
|
173
|
+
process.stdin.on('data', (data) => {
|
|
174
|
+
const key = data.toString();
|
|
175
|
+
const from = ui.view;
|
|
176
|
+
const next = press(ui, key, { open: judges.flatMap((j, i) => (j ? [i + 1] : [])), running: rows.some((r) => r.state === 'running') });
|
|
177
|
+
ui = { view: next.view, confirm: next.confirm };
|
|
178
|
+
if (next.send) {
|
|
179
|
+
judges[from - 1].proc.write(key);
|
|
180
|
+
} else if (next.quit) {
|
|
181
|
+
clearInterval(timer);
|
|
182
|
+
clearInterval(spinner);
|
|
183
|
+
// The harness on PATH can be a shell script that runs the real binary as a child and waits for it, so
|
|
184
|
+
// SIGHUP goes to the process group of the judge. The pty makes each judge the leader of its own group.
|
|
185
|
+
for (const j of judges) if (j && !j.exited) process.kill(-j.proc.pid, 'SIGHUP');
|
|
186
|
+
out.write(`${TABLE_MODES}\x1b[?25h\x1b[?1049l`);
|
|
187
|
+
process.stdin.setRawMode(false);
|
|
188
|
+
process.stdin.pause();
|
|
189
|
+
done();
|
|
190
|
+
} else if (ui.view) {
|
|
191
|
+
showJudge(ui.view);
|
|
192
|
+
} else {
|
|
193
|
+
if (from) out.write(TABLE_MODES);
|
|
194
|
+
draw();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
out.on('resize', () => {
|
|
198
|
+
const { cols, rows: lines } = size();
|
|
199
|
+
for (const j of judges) {
|
|
200
|
+
if (j && !j.exited) j.proc.resize(cols, lines);
|
|
201
|
+
j?.term.resize(cols, lines);
|
|
202
|
+
}
|
|
203
|
+
if (ui.view) showJudge(ui.view);
|
|
204
|
+
else draw();
|
|
205
|
+
});
|
|
206
|
+
tick();
|
|
113
207
|
});
|
|
114
|
-
process.stdout.on('resize', draw);
|
|
115
|
-
tick();
|
|
116
|
-
setInterval(tick, 1000);
|
|
117
208
|
}
|
|
118
|
-
|
|
119
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) main(process.argv[2]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsft/panels",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Run a panel of AI judges from cross.software/panels against a target.",
|
|
6
6
|
"type": "module",
|
|
@@ -8,5 +8,10 @@
|
|
|
8
8
|
"files": ["bin", "lib", "prompts", "schema", "README.md"],
|
|
9
9
|
"license": "UNLICENSED",
|
|
10
10
|
"engines": { "node": ">=20" },
|
|
11
|
-
"scripts": { "test": "node --test test/*.test.js" }
|
|
11
|
+
"scripts": { "test": "node --test test/*.test.js" },
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@homebridge/node-pty-prebuilt-multiarch": "0.14.1",
|
|
14
|
+
"@xterm/addon-serialize": "0.14.0",
|
|
15
|
+
"@xterm/headless": "6.0.0"
|
|
16
|
+
}
|
|
12
17
|
}
|