@xsft/panels 0.7.0 → 0.7.1
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 +6 -4
- package/bin/panels.js +7 -1
- package/lib/args.js +5 -1
- package/lib/judge.js +15 -20
- package/lib/report.js +7 -2
- package/lib/table.js +176 -61
- package/package.json +19 -6
- package/prompts/judge-system-prompt.txt +2 -2
- package/schema/report.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ panels run <panel> [target...] [flags]
|
|
|
11
11
|
--harness <name> claude | codex default: claude if installed, else codex
|
|
12
12
|
--brief <file> the brief default: none
|
|
13
13
|
--sources <path> a doc folder, a repository, or an app URL; repeat the flag for each
|
|
14
|
+
--yolo the judges skip every permission question (claude --dangerously-skip-permissions, codex --yolo).
|
|
15
|
+
Without it, a judge asks its own questions in its tab. Without a terminal, the flag is required.
|
|
14
16
|
-- end of the flags; each later word joins the target
|
|
15
17
|
|
|
16
18
|
exit 0 pass: every criterion scored 4
|
|
@@ -18,7 +20,7 @@ exit 1 fail: every judge answered, and one or more criteria scored below 4
|
|
|
18
20
|
exit 2 error: bad flags, no target, panel not found, harness missing, or a judge failed
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
CAUTION: A judge
|
|
23
|
+
CAUTION: A judge gets no tool block and no sandbox from the CLI. Without `--yolo`, the harness asks its own permission questions in the judge tab. With `--yolo`, nothing asks: the judge can read, write, run commands, and reach the network as you, and the system prompt (do not write or change files, no sub-agents, no forms, no posts) is the only guard. Do not run a panel on a repository that you do not trust.
|
|
22
24
|
|
|
23
25
|
```sh
|
|
24
26
|
npx @xsft/panels run xsft/marketing-site https://example.com --brief docs/brief.md --harness claude
|
|
@@ -29,11 +31,11 @@ Run the command in the folder of the product. Each judge starts in that folder,
|
|
|
29
31
|
|
|
30
32
|
## Terminal mode
|
|
31
33
|
|
|
32
|
-
On a terminal, `panels run` shows the overview, 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 overview 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`), and stdout prints `N/4 name` and the verdict of each criterion, then the report path.
|
|
34
|
+
On a terminal, `panels run` shows the overview, 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 overview 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`), and stdout prints `N/4 name` and the verdict of each criterion, then the report path. Plain mode needs `--yolo`, because a background judge cannot answer a question; without it the command exits 2.
|
|
33
35
|
|
|
34
|
-
The overview
|
|
36
|
+
The overview fills the terminal: a header line with the run (panel and target at the left, the version, the harness, and the elapsed time at the right edge) and two columns from 100 columns up (stacked below that). The left column lists the criteria: rank, name, score (4 green, 3 ink, 2 yellow, 1 red, a grey `·/4` until the score arrives), the time the judge has run, and the tokens it used, with a total row at the bottom under a hairline: the scores added as quarters over the number of criteria, the longest judge time, and the tokens added up. The right column shows the criterion under the cursor: its name, score, and level word, the verdict paragraph, the one-line description and the body of the criterion as plain text, and at the bottom the key line and a grey fact line with the judge number, the harness, the time, and the answer file. `enter` moves the focus into that pane (an orange bar marks it): `↑` and `↓` scroll one line, `PgUp` and `PgDn` a page, `esc` or `enter` return to the list. While a judge runs, its detail shows the word judging in big letters (figlet, ANSI Shadow) with the wave over it and the time under it. One soft wave sweeps across the running rows. On the overview, `↑` and `↓` move the cursor, `r` restarts the judge of the cursor row with its harness (its answer is dropped and the report is written again when it finishes), and `h` flips that harness between `claude` and `codex`. The page never says pass or fail; the total is the result.
|
|
35
37
|
|
|
36
|
-
A tab strip on the last three lines shows a hairline, the tabs (`
|
|
38
|
+
A tab strip on the last three lines shows a hairline, the tabs (`Overview`, then the name of each criterion, split by a grey `│`, cut to the first word when they do not fit), and a status line with the progress, the elapsed time, and the keys. The strip is the navigation area. With the focus on the strip, `←` and `→` move the active tab and switch the view, `1` to `9` jump to a started judge by list order, `0` to the overview, `enter` gives the focus to the judge, and `q` quits; while a judge still runs, the first `q` asks and a second `q` quits. With the focus on a judge, each key goes to the judge, and `ctrl-q` gives the focus back to the strip. The overview takes no text, so the strip keeps the focus there. 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.
|
|
37
39
|
|
|
38
40
|
## Run folder
|
|
39
41
|
|
package/bin/panels.js
CHANGED
|
@@ -29,7 +29,7 @@ const EXIT = { pass: 0, fail: 1, error: 2 };
|
|
|
29
29
|
|
|
30
30
|
/** Runs the judges in the score table, and exits from report.json when the engineer quits. */
|
|
31
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 });
|
|
32
|
+
await runTable({ panel, runDir, harness, cwd, target: args.target, brief: args.brief, sources: args.sources, yolo: args.yolo });
|
|
33
33
|
const reportPath = join(runDir, 'report.json');
|
|
34
34
|
if (!existsSync(reportPath)) return 2;
|
|
35
35
|
console.log(reportPath);
|
|
@@ -55,6 +55,11 @@ async function main() {
|
|
|
55
55
|
console.error(`panels: harness missing: ${harness} is not on PATH`);
|
|
56
56
|
return 2;
|
|
57
57
|
}
|
|
58
|
+
// A judge without a terminal cannot answer a permission question.
|
|
59
|
+
if (!(process.stdout.isTTY && process.stdin.isTTY) && !args.yolo) {
|
|
60
|
+
console.error('panels: plain mode needs --yolo (judges cannot ask)');
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
58
63
|
const fetched = await fetchPanel(args.panel);
|
|
59
64
|
if (fetched.error) {
|
|
60
65
|
console.error(`panels: ${fetched.error}`);
|
|
@@ -75,6 +80,7 @@ async function main() {
|
|
|
75
80
|
target: args.target,
|
|
76
81
|
brief: args.brief,
|
|
77
82
|
sources: args.sources,
|
|
83
|
+
yolo: true,
|
|
78
84
|
}));
|
|
79
85
|
const report = buildReport({ panel, target: args.target, outcomes });
|
|
80
86
|
const reportPath = join(runDir, 'report.json');
|
package/lib/args.js
CHANGED
|
@@ -8,6 +8,8 @@ export const SYNOPSIS = `panels run <panel> [target...] [flags]
|
|
|
8
8
|
--harness <name> claude | codex default: claude if installed, else codex
|
|
9
9
|
--brief <file> the brief default: none
|
|
10
10
|
--sources <path> a doc folder, a repository, or an app URL; repeat the flag for each
|
|
11
|
+
--yolo the judges skip every permission question (claude --dangerously-skip-permissions, codex --yolo).
|
|
12
|
+
Without it, a judge asks its own questions in its tab. Without a terminal, the flag is required.
|
|
11
13
|
-- end of the flags; each later word joins the target
|
|
12
14
|
|
|
13
15
|
exit 0 pass: every criterion scored 4
|
|
@@ -18,6 +20,7 @@ const options = {
|
|
|
18
20
|
harness: { type: 'string' },
|
|
19
21
|
brief: { type: 'string' },
|
|
20
22
|
sources: { type: 'string', multiple: true },
|
|
23
|
+
yolo: { type: 'boolean' },
|
|
21
24
|
help: { type: 'boolean' },
|
|
22
25
|
};
|
|
23
26
|
|
|
@@ -29,7 +32,7 @@ export function parse(argv) {
|
|
|
29
32
|
const { values, positionals, tokens } = parseArgs({ args: argv, options, strict: false, allowPositionals: true, tokens: true });
|
|
30
33
|
for (const token of tokens.filter((t) => t.kind === 'option')) {
|
|
31
34
|
if (!Object.hasOwn(options, token.name)) return { error: `unknown flag ${token.rawName}` };
|
|
32
|
-
if (token.name
|
|
35
|
+
if (options[token.name].type === 'string' && token.value === undefined) return { error: `${token.rawName} needs a value` };
|
|
33
36
|
}
|
|
34
37
|
if (values.help) return { help: true };
|
|
35
38
|
const [command, panel, ...words] = positionals;
|
|
@@ -44,5 +47,6 @@ export function parse(argv) {
|
|
|
44
47
|
harness: values.harness,
|
|
45
48
|
brief: values.brief,
|
|
46
49
|
sources: values.sources ?? [],
|
|
50
|
+
yolo: Boolean(values.yolo),
|
|
47
51
|
};
|
|
48
52
|
}
|
package/lib/judge.js
CHANGED
|
@@ -3,12 +3,15 @@ import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'nod
|
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
|
|
5
5
|
const { criterion: CRITERION } = JSON.parse(readFileSync(new URL('../schema/report.json', import.meta.url), 'utf8')).$defs;
|
|
6
|
-
const VERDICT_LENGTH = CRITERION.properties.verdict.maxLength;
|
|
7
6
|
const SYSTEM_PROMPT = readFileSync(new URL('../prompts/judge-system-prompt.txt', import.meta.url), 'utf8');
|
|
8
7
|
export const MINUTES_PER_CRITERION = 20;
|
|
9
|
-
|
|
10
|
-
// With --
|
|
11
|
-
|
|
8
|
+
// A judge gets no tool list, no permission mode, and no sandbox flag: the harness defaults, and its own questions
|
|
9
|
+
// in its tab. With --yolo, the judge skips every question: no sandbox, nothing is denied, nothing asks. The system
|
|
10
|
+
// prompt holds the rules on writing files, sub-agents, forms, and messages. exec resume takes no sandbox flag,
|
|
11
|
+
// so it gets the same setting as config values.
|
|
12
|
+
// codex --yolo is the alias of --dangerously-bypass-approvals-and-sandbox.
|
|
13
|
+
const yoloFlags = (yolo, harness) => (!yolo ? [] : harness === 'claude' ? ['--dangerously-skip-permissions'] : ['--yolo']);
|
|
14
|
+
const CODEX_RESUME_YOLO = ['-c', 'sandbox_mode="danger-full-access"', '-c', 'approval_policy="never"'];
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* The user message of a judge. criterion is { rank, name, description, body }. Each part sits in a tag,
|
|
@@ -35,7 +38,6 @@ export function check(answer, criterion) {
|
|
|
35
38
|
if (answer.name !== criterion.name) errors.push(`name must be "${criterion.name}".`);
|
|
36
39
|
if (!Number.isInteger(answer.score) || answer.score < 1 || answer.score > 4) errors.push('score must be a whole number from 1 to 4.');
|
|
37
40
|
if (typeof answer.verdict !== 'string' || !answer.verdict.trim()) errors.push('verdict must be one paragraph of text.');
|
|
38
|
-
else if (answer.verdict.length > VERDICT_LENGTH) errors.push(`verdict must be ${VERDICT_LENGTH} characters at most; it has ${answer.verdict.length}.`);
|
|
39
41
|
return errors;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -62,10 +64,7 @@ function exec(command, args, { cwd, stdin, timeout }) {
|
|
|
62
64
|
|
|
63
65
|
async function askClaude(j, prompt, session) {
|
|
64
66
|
const args = [
|
|
65
|
-
'-p', '
|
|
66
|
-
'--tools', TOOLS,
|
|
67
|
-
'--allowedTools', ALLOWED_TOOLS,
|
|
68
|
-
'--permission-mode', 'dontAsk', '--permission-prompts', 'none',
|
|
67
|
+
'-p', ...yoloFlags(j.yolo, 'claude'),
|
|
69
68
|
...j.addDirs.flatMap((d) => ['--add-dir', d]),
|
|
70
69
|
'--system-prompt', SYSTEM_PROMPT,
|
|
71
70
|
'--json-schema', JSON.stringify(j.schema),
|
|
@@ -82,15 +81,13 @@ async function askClaude(j, prompt, session) {
|
|
|
82
81
|
return { answer: result.structured_output, session: result.session_id };
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
// --disable apps: the apps feature gives Codex the connectors of the ChatGPT account, and some of them write
|
|
86
|
-
// (chatgpt_space_create_page, chatgpt_space_trash_page). --ignore-user-config does not turn them off.
|
|
87
84
|
async function askCodex(j, prompt, thread) {
|
|
88
85
|
const raw = join(j.dir, 'raw.json');
|
|
89
86
|
const instructions = `developer_instructions=${JSON.stringify(SYSTEM_PROMPT)}`;
|
|
90
87
|
const output = ['--output-schema', join(j.dir, 'schema.json'), '-o', raw, '--json'];
|
|
91
88
|
const args = thread
|
|
92
|
-
? ['exec', 'resume', thread, prompt, '--skip-git-repo-check', '--ignore-user-config',
|
|
93
|
-
: ['exec', '-', '-C', j.cwd, '--skip-git-repo-check', '--ignore-user-config',
|
|
89
|
+
? ['exec', 'resume', thread, prompt, '--skip-git-repo-check', '--ignore-user-config', ...(j.yolo ? CODEX_RESUME_YOLO : []), '-c', instructions, ...output]
|
|
90
|
+
: ['exec', '-', '-C', j.cwd, '--skip-git-repo-check', '--ignore-user-config', ...yoloFlags(j.yolo, 'codex'), '-c', instructions, ...output];
|
|
94
91
|
const stdin = thread ? 'ignore' : openSync(join(j.dir, 'prompt.md'));
|
|
95
92
|
const run = await exec(j.command, args, { cwd: j.cwd, stdin, timeout: j.timeout });
|
|
96
93
|
if (!thread) closeSync(stdin);
|
|
@@ -112,13 +109,13 @@ export function parseAnswer(text) {
|
|
|
112
109
|
* Runs one judge to the end: the first answer, and one re-ask when check() rejects it.
|
|
113
110
|
* Returns { answer } or { error }. command is the harness binary; the tests pass a stub.
|
|
114
111
|
*/
|
|
115
|
-
export async function runJudge({ dir, criterion, harness, cwd, target, brief, sources }, command = harness) {
|
|
112
|
+
export async function runJudge({ dir, criterion, harness, cwd, target, brief, sources, yolo = false }, command = harness) {
|
|
116
113
|
mkdirSync(dir, { recursive: true });
|
|
117
114
|
const prompt = userMessage(criterion, { target, brief, sources });
|
|
118
115
|
const schema = judgeSchema(criterion);
|
|
119
116
|
writeFileSync(join(dir, 'prompt.md'), prompt);
|
|
120
117
|
writeFileSync(join(dir, 'schema.json'), JSON.stringify(schema, null, 2));
|
|
121
|
-
const j = { dir, cwd, schema, command, addDirs: addDirs(cwd, brief, sources), timeout: MINUTES_PER_CRITERION * 60_000 };
|
|
118
|
+
const j = { dir, cwd, schema, command, yolo, addDirs: addDirs(cwd, brief, sources), timeout: MINUTES_PER_CRITERION * 60_000 };
|
|
122
119
|
const ask = harness === 'claude' ? askClaude : askCodex;
|
|
123
120
|
let reply = await ask(j, prompt);
|
|
124
121
|
if (reply.error) return reply;
|
|
@@ -140,7 +137,7 @@ export const tomlString = (s) => `"${s.replace(/["\\\x00-\x1f\x7f]/g, (c) => TOM
|
|
|
140
137
|
* Writes prompt.md and schema.json of one judge of the terminal mode, and returns the command and the
|
|
141
138
|
* arguments that start the judge in its interactive harness.
|
|
142
139
|
*/
|
|
143
|
-
export function writeJudge({ dir, criterion, harness, cwd, target, brief, sources }) {
|
|
140
|
+
export function writeJudge({ dir, criterion, harness, cwd, target, brief, sources, yolo = false }) {
|
|
144
141
|
mkdirSync(dir, { recursive: true });
|
|
145
142
|
const closing = `The engineer ran the command in ${cwd}. As your last step, write your answer, one JSON object { name, score, verdict } that 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`;
|
|
146
143
|
const prompt = `${userMessage(criterion, { target, brief, sources })}\n${closing}`;
|
|
@@ -149,10 +146,8 @@ export function writeJudge({ dir, criterion, harness, cwd, target, brief, source
|
|
|
149
146
|
if (harness === 'codex') {
|
|
150
147
|
// Codex ranks the developer instructions above the prompt, and the system prompt says to return the JSON and to
|
|
151
148
|
// write no files. Without the closing in the developer instructions, the judge prints the JSON and writes no answer.json.
|
|
152
|
-
return ['codex', '-C', dir,
|
|
149
|
+
return ['codex', '-C', dir, ...yoloFlags(yolo, 'codex'), '-c', `developer_instructions=${tomlString(`${SYSTEM_PROMPT}\n${closing}`)}`, prompt];
|
|
153
150
|
}
|
|
154
|
-
// Claude matches file paths only in Edit rules, which cover Write too; // starts an absolute path.
|
|
155
|
-
const allowed = `${ALLOWED_TOOLS},Edit(/${join(dir, 'answer.json')})`;
|
|
156
151
|
const add = addDirs(cwd, brief, sources).flatMap((d) => ['--add-dir', d]);
|
|
157
|
-
return ['claude', '
|
|
152
|
+
return ['claude', ...yoloFlags(yolo, 'claude'), '--system-prompt', SYSTEM_PROMPT, ...add, prompt];
|
|
158
153
|
}
|
package/lib/report.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/** The version of this package, from package.json. */
|
|
4
|
+
export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Builds report.json: { panel: "owner/name@version", target, status, criteria }. outcomes holds the
|
|
3
8
|
* { answer } or { error } of the judge of each criterion, in panel order. A criterion whose judge failed
|
|
@@ -13,7 +18,7 @@ export function buildReport({ panel, target, outcomes }) {
|
|
|
13
18
|
return { panel: `${panel.owner}/${panel.name}@${panel.version}`, target, status, criteria };
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
/** The lines that the plain mode prints: "N/4 name" and the verdict under it, for each criterion. */
|
|
21
|
+
/** The lines that the plain mode prints: the run and the version, then "N/4 name" and the verdict under it, for each criterion. */
|
|
17
22
|
export function renderPlain(report) {
|
|
18
|
-
return report.criteria.flatMap((c) => [`${c.score === null ? 'err' : `${c.score}/4`} ${c.name}`, ` ${c.verdict}`, '']);
|
|
23
|
+
return [`panels ${VERSION} ${report.panel} ${report.target}`, '', ...report.criteria.flatMap((c) => [`${c.score === null ? 'err' : `${c.score}/4`} ${c.name}`, ` ${c.verdict}`, ''])];
|
|
19
24
|
}
|
package/lib/table.js
CHANGED
|
@@ -5,8 +5,9 @@ import { join } from 'node:path';
|
|
|
5
5
|
import pty from '@homebridge/node-pty-prebuilt-multiarch';
|
|
6
6
|
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
7
7
|
import xterm from '@xterm/headless';
|
|
8
|
+
import figlet from 'figlet';
|
|
8
9
|
import { check, MINUTES_PER_CRITERION, parseAnswer, writeJudge } from './judge.js';
|
|
9
|
-
import { buildReport } from './report.js';
|
|
10
|
+
import { buildReport, VERSION } from './report.js';
|
|
10
11
|
|
|
11
12
|
const LIVE_JUDGES = 10;
|
|
12
13
|
// A harness asks this in a folder it has not seen. Only the first judge starts until its screen no longer asks,
|
|
@@ -75,6 +76,16 @@ export function tokensOnScreen(text) {
|
|
|
75
76
|
return best;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
let artLines;
|
|
80
|
+
/** The word "judging" as big letters, in the ANSI Shadow font (Standard when that font is missing). Rendered once. */
|
|
81
|
+
export function art() {
|
|
82
|
+
if (!artLines) {
|
|
83
|
+
const font = figlet.fontsSync().includes('ANSI Shadow') ? 'ANSI Shadow' : 'Standard';
|
|
84
|
+
artLines = figlet.textSync('judging', { font }).split('\n').filter((l) => l.trim());
|
|
85
|
+
}
|
|
86
|
+
return artLines;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
const plainLength = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
79
90
|
const padTo = (s, width) => s + ' '.repeat(Math.max(0, width - plainLength(s)));
|
|
80
91
|
|
|
@@ -107,17 +118,95 @@ export function wrap(text, width) {
|
|
|
107
118
|
const quarters = (sum) => String(Math.round((sum / 4) * 100) / 100);
|
|
108
119
|
|
|
109
120
|
/**
|
|
110
|
-
* The
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
* The body of a criterion as plain lines for the detail pane: a heading becomes a grey upper-case label, a list
|
|
122
|
+
* item keeps its dash, a fenced block is indented four spaces as it is, a table line stays as it is, and the
|
|
123
|
+
* other text is wrapped to width with the emphasis, code, and link marks removed.
|
|
124
|
+
*/
|
|
125
|
+
export function plainMarkdown(markdown, width) {
|
|
126
|
+
const inline = (t) => t.replace(/\*\*(.+?)\*\*/g, '$1').replace(/__(.+?)__/g, '$1').replace(/`([^`]+)`/g, '$1').replace(/\*(\S[^*]*?)\*/g, '$1').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
|
127
|
+
const lines = [];
|
|
128
|
+
let paragraph = [];
|
|
129
|
+
let fence = false;
|
|
130
|
+
const flush = () => {
|
|
131
|
+
if (paragraph.length) lines.push(...wrap(inline(paragraph.join(' ')), width));
|
|
132
|
+
paragraph = [];
|
|
133
|
+
};
|
|
134
|
+
for (const raw of markdown.split('\n')) {
|
|
135
|
+
const line = raw.trimEnd();
|
|
136
|
+
if (/^\s*```/.test(line)) { flush(); fence = !fence; continue; }
|
|
137
|
+
if (fence) { lines.push(` ${line}`); continue; }
|
|
138
|
+
if (!line.trim()) { flush(); if (lines.at(-1) !== '') lines.push(''); continue; }
|
|
139
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
140
|
+
if (heading) { flush(); if (lines.length && lines.at(-1) !== '') lines.push(''); lines.push(`${GREY}${inline(heading[1]).toUpperCase()}${RESET}`); continue; }
|
|
141
|
+
const item = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/.exec(line);
|
|
142
|
+
if (item) { flush(); const [first, ...rest] = wrap(inline(item[1]), width - 2); lines.push(`- ${first ?? ''}`, ...rest.map((l) => ` ${l}`)); continue; }
|
|
143
|
+
if (/^\s*\|/.test(line)) { flush(); lines.push(inline(line)); continue; }
|
|
144
|
+
paragraph.push(line.trim());
|
|
145
|
+
}
|
|
146
|
+
flush();
|
|
147
|
+
while (lines.at(-1) === '') lines.pop();
|
|
148
|
+
while (lines[0] === '') lines.shift();
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The lines of the detail pane under its header: the verdict or the judging art, then the criterion text. */
|
|
153
|
+
function detailContent(row, columnWidth, start, width, frame) {
|
|
154
|
+
const lines = [''];
|
|
155
|
+
if (row.state === 'running' && !row.trust) {
|
|
156
|
+
// The judge works: the word judging in big letters with the wave over it, or in one line in a narrow pane.
|
|
157
|
+
const letters = art();
|
|
158
|
+
lines.push(...(letters[0].length <= columnWidth ? letters : ['judging']).map((l) => glimmer(l, frame, start, width)));
|
|
159
|
+
const since = row.restarted !== null ? `restarted with ${row.harness} · ` : '';
|
|
160
|
+
lines.push(`${GREY}${since}${clock(row.time ?? 0)} · ${row.rank} to watch${RESET}`);
|
|
161
|
+
} else {
|
|
162
|
+
const text = row.state === 'done' ? row.result.verdict
|
|
163
|
+
: row.state === 'error' ? row.error
|
|
164
|
+
: row.state === 'queued' ? 'The judge has not started.'
|
|
165
|
+
: `Press ${row.rank} and answer the trust prompt.`;
|
|
166
|
+
lines.push(...wrap(text, columnWidth));
|
|
167
|
+
}
|
|
168
|
+
if (row.description) lines.push('', ...wrap(row.description, columnWidth));
|
|
169
|
+
if (row.body) lines.push('', ...plainMarkdown(row.body, columnWidth));
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const layout = (rows, width) => {
|
|
174
|
+
// The name column is as wide as the longest name, and at least as wide as the word total under it.
|
|
175
|
+
const leftWidth = Math.max(5, ...rows.map((r) => r.name.length)) + 27;
|
|
176
|
+
return width >= TWO_COLUMNS ? { leftWidth, columnWidth: width - leftWidth - 3, start: leftWidth + 3 } : { leftWidth, columnWidth: width - 2, start: 2 };
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** The number of lines of the detail pane that the scroll can hide: the content past the window of the pane. */
|
|
180
|
+
export function maxScroll({ rows, cursor, width, height }) {
|
|
181
|
+
const { columnWidth, start } = layout(rows, width);
|
|
182
|
+
const content = detailContent(rows[cursor], columnWidth, start, width, 0);
|
|
183
|
+
const body = height - 2;
|
|
184
|
+
const paneHeight = width >= TWO_COLUMNS ? body : body - Math.floor(body / 2) - 1;
|
|
185
|
+
// The pane holds the header, the content window, a blank line, and the three bottom lines.
|
|
186
|
+
const window = paneHeight - 5;
|
|
187
|
+
return window < 1 ? 0 : Math.max(0, content.length - window);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The lines of the overview. rows hold { rank, name, description, body, state, result, error, trust, time, tokens,
|
|
192
|
+
* harness, restarted }, and state is queued, running, done, or error. time is the seconds the judge has run,
|
|
193
|
+
* tokens the count it used (null for none), harness the one its next restart uses, restarted the seconds since
|
|
194
|
+
* a restart (null for none). cursor is the index of the row whose detail shows at the right; focus is 'list'
|
|
195
|
+
* or 'detail' (the detail pane shows an orange bar and its header turns orange); scroll hides that many lines
|
|
196
|
+
* of the detail content under its header. The header line holds the run: panel and target at the left, the
|
|
197
|
+
* version, the harness, and the elapsed time at the right edge. Two columns from TWO_COLUMNS wide, the list
|
|
198
|
+
* above the detail below that. height is the number of lines the overview may fill: the column divider runs
|
|
199
|
+
* the whole height, the rows and the detail header stay at the top, and the total row and the key and fact
|
|
200
|
+
* lines sit at the bottom. Without a height, or when the content is taller than the height, the lines follow
|
|
201
|
+
* each other and nothing scrolls.
|
|
115
202
|
*/
|
|
116
|
-
export function renderOverview({ title, target, harness, elapsed, rows, cursor, width, frame = 0 }) {
|
|
117
|
-
const
|
|
203
|
+
export function renderOverview({ title, target, harness, elapsed, rows, cursor, width, height, frame = 0, version = VERSION, focus = 'list', scroll = 0 }) {
|
|
204
|
+
const run = ` panels run ${title} ${target}`;
|
|
205
|
+
const meta = `${version} · ${harness} · ${clock(elapsed)} `;
|
|
206
|
+
const header = ` ${BOLD}panels run${RESET} ${title}${GREY} ${target}${' '.repeat(Math.max(2, width - run.length - meta.length))}${meta}${RESET}`;
|
|
118
207
|
const rule = `${GREY}${'─'.repeat(width)}${RESET}`;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
208
|
+
const { leftWidth, columnWidth, start } = layout(rows, width);
|
|
209
|
+
const pad = leftWidth - 27;
|
|
121
210
|
const cells = (score, time, tokens) => ` ${score.padStart(3)} ${time.padStart(5)} ${tokens.padStart(6)}`;
|
|
122
211
|
const left = rows.map((r, i) => {
|
|
123
212
|
const current = i === cursor;
|
|
@@ -127,59 +216,71 @@ export function renderOverview({ title, target, harness, elapsed, rows, cursor,
|
|
|
127
216
|
const tokens = tokensText(r.tokens);
|
|
128
217
|
if (r.state === 'running' && !r.trust) {
|
|
129
218
|
const name = glimmer(r.name.padEnd(pad), frame, 6, width, current ? GLIMMER_ORANGE : GLIMMER);
|
|
130
|
-
return `${mark}${current ? ORANGE : ''}${rank}${RESET}${name}${current ? ORANGE : GREY}${cells('', time, tokens)}${RESET}`;
|
|
219
|
+
return `${mark}${current ? ORANGE : ''}${rank}${RESET}${name}${current ? ORANGE : GREY}${cells('·/4', time, tokens)}${RESET}`;
|
|
131
220
|
}
|
|
132
|
-
|
|
133
|
-
const
|
|
221
|
+
// A row without a score yet holds the place of the score with ·/4.
|
|
222
|
+
const score = r.state === 'done' ? `${r.result.score}/4` : r.state === 'error' ? '✗' : '·/4';
|
|
223
|
+
const colour = current ? ORANGE : r.state === 'done' ? SCORE_COLOUR[r.result.score] : r.state === 'error' ? SCORE_COLOUR[1] : GREY;
|
|
134
224
|
const name = current ? `${ORANGE}${r.name.padEnd(pad)}${RESET}` : r.state === 'queued' ? `${GREY}${r.name.padEnd(pad)}${RESET}` : r.name.padEnd(pad);
|
|
135
225
|
const rest = r.state === 'running' ? `${ORANGE}press ${r.rank} and answer the trust prompt${RESET}` : `${colour}${cells(score, time, tokens)}${RESET}`;
|
|
136
226
|
return `${mark}${current ? ORANGE : ''}${rank}${RESET}${name}${rest}`;
|
|
137
227
|
});
|
|
138
228
|
const done = rows.filter((r) => r.state === 'done');
|
|
139
|
-
const
|
|
140
|
-
|
|
229
|
+
const sum = done.length ? `${quarters(done.reduce((n, r) => n + r.result.score, 0))}/${rows.length}` : `·/${rows.length}`;
|
|
230
|
+
const total = cells(sum, clock(Math.max(0, ...rows.map((r) => r.time ?? 0))), tokensText(rows.some((r) => r.tokens !== null) ? rows.reduce((n, r) => n + (r.tokens ?? 0), 0) : null));
|
|
231
|
+
const totalLine = `${padTo(' total', leftWidth - total.length)}${done.length ? total : total.replace(sum, `${GREY}${sum}${RESET}`)}`;
|
|
232
|
+
const leftBottom = [`${GREY}${'─'.repeat(leftWidth)}${RESET}`, totalLine];
|
|
141
233
|
|
|
142
234
|
const row = rows[cursor];
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
235
|
+
const level = row.state === 'done' ? `${row.result.score}/4 ${LEVELS[row.result.score]}` : row.state === 'error' ? 'error' : row.state;
|
|
236
|
+
const name = `${row.rank} ${row.name.toUpperCase()}`;
|
|
237
|
+
const detailHeader = `${focus === 'detail' ? `${ORANGE}${BOLD}` : BOLD}${name}${RESET}${' '.repeat(Math.max(1, columnWidth - name.length - level.length))}${GREY}${level}${RESET}`;
|
|
238
|
+
const content = detailContent(row, columnWidth, start, width, frame);
|
|
239
|
+
const facts = [`judge ${row.rank}`, row.harness, ...(row.time === null ? [] : [clock(row.time)]), ...(row.state === 'done' || row.state === 'error' ? [`judges/${row.rank}/answer.json`] : [])];
|
|
240
|
+
const bottom = [`${ORANGE}r${RESET} restart ${ORANGE}h${RESET} harness ${row.harness} ${ORANGE}${row.rank}${RESET} watch`, '', `${GREY}${facts.join(' · ')}${RESET}`];
|
|
241
|
+
|
|
242
|
+
// top, gap blank lines, then bottom; with a height that holds them, the blank lines grow so bottom ends at the height.
|
|
243
|
+
const pane = (top, bottomLines, lines, gap) => {
|
|
244
|
+
const fill = lines === undefined || lines < top.length + gap + bottomLines.length ? gap : lines - top.length - bottomLines.length;
|
|
245
|
+
return [...top, ...Array(fill).fill(''), ...bottomLines];
|
|
246
|
+
};
|
|
247
|
+
// The detail pane: the header, a window of the content that the scroll moves, a blank line, then the bottom lines.
|
|
248
|
+
const detailPane = (lines) => {
|
|
249
|
+
const window = lines === undefined ? 0 : lines - 5;
|
|
250
|
+
if (window < 1) return pane([detailHeader, ...content], bottom, lines, 1);
|
|
251
|
+
const from = Math.min(scroll, Math.max(0, content.length - window));
|
|
252
|
+
return [detailHeader, ...content.slice(from, from + window), ...Array(Math.max(0, window - content.length)).fill(''), '', ...bottom];
|
|
158
253
|
};
|
|
254
|
+
const edge = focus === 'detail' ? `${ORANGE}▌${RESET}` : `${GREY}│${RESET}`;
|
|
159
255
|
|
|
160
256
|
const lines = [header, rule];
|
|
257
|
+
const body = height === undefined ? undefined : height - 2;
|
|
161
258
|
if (width >= TWO_COLUMNS) {
|
|
162
|
-
|
|
163
|
-
|
|
259
|
+
const leftLines = pane(left, leftBottom, body, 0);
|
|
260
|
+
const rightLines = detailPane(body);
|
|
261
|
+
for (let i = 0; i < Math.max(leftLines.length, rightLines.length); i++) lines.push(`${padTo(leftLines[i] ?? '', leftWidth)} ${edge} ${rightLines[i] ?? ''}`);
|
|
164
262
|
} else {
|
|
165
|
-
|
|
166
|
-
|
|
263
|
+
const half = body === undefined ? undefined : Math.floor(body / 2);
|
|
264
|
+
const list = pane(left, leftBottom, half, 0);
|
|
265
|
+
lines.push(...list, '', ...detailPane(body === undefined ? undefined : body - list.length - 1).map((l) => `${focus === 'detail' ? `${ORANGE}▌${RESET}` : ' '} ${l}`));
|
|
167
266
|
}
|
|
168
267
|
return lines.map((l) => clip(l, width));
|
|
169
268
|
}
|
|
170
269
|
|
|
171
270
|
/**
|
|
172
271
|
* The three lines of the tab strip: a grey hairline, the tabs, then a grey status line with the progress and
|
|
173
|
-
* the elapsed time at the left and the keys of the focus at the right. The first tab is
|
|
272
|
+
* the elapsed time at the left and the keys of the focus at the right. The first tab is Overview, then the
|
|
174
273
|
* name of each criterion, with a grey │ between the tabs. A running tab glimmers. The names step down to the
|
|
175
274
|
* first word when they do not fit the width. focus is 'strip' (the active tab is bold orange, the arrows move
|
|
176
|
-
* it) or 'judge' (the active tab is bold ink, the keys go to
|
|
177
|
-
* the quit question.
|
|
275
|
+
* it), 'detail' (the detail pane of the overview scrolls), or 'judge' (the active tab is bold ink, the keys go to
|
|
276
|
+
* the judge). confirm replaces the progress with the quit question.
|
|
178
277
|
*/
|
|
179
278
|
export function renderStrip({ rows, view, frame = 0, width, focus = 'strip', elapsed = 0, confirm = false }) {
|
|
180
279
|
const finished = rows.filter((r) => r.state === 'done' || r.state === 'error').length;
|
|
181
|
-
const keys = focus === 'judge' ? 'ctrl-q tabs'
|
|
182
|
-
|
|
280
|
+
const keys = focus === 'judge' ? 'ctrl-q tabs'
|
|
281
|
+
: focus === 'detail' ? '↑ ↓ scroll · enter list · ← → tabs · q q quit'
|
|
282
|
+
: view === 0 ? '↑ ↓ criterion · r restart · h harness · ← → tabs · q q quit' : '← → tabs · enter back · q quit';
|
|
283
|
+
const names = (level) => ['Overview', ...rows.map((r) => (level === 0 ? r.name : r.name.split(' ')[0]))];
|
|
183
284
|
const used = (list) => 1 + list.reduce((n, t) => n + t.length, 0) + 3 * (list.length - 1);
|
|
184
285
|
const list = [0, 1].map(names).find((l) => used(l) <= width) ?? names(1);
|
|
185
286
|
let line = ' ';
|
|
@@ -204,40 +305,52 @@ export function renderStrip({ rows, view, frame = 0, width, focus = 'strip', ela
|
|
|
204
305
|
}
|
|
205
306
|
|
|
206
307
|
/**
|
|
207
|
-
* The next { view, confirm, focus, cursor } after one key, plus send: true when the key goes to the judge,
|
|
308
|
+
* The next { view, confirm, focus, cursor, scroll } after one key, plus send: true when the key goes to the judge,
|
|
208
309
|
* quit: true, restart: N to restart judge N, or flip: N to flip the harness of judge N. view is 0 for the
|
|
209
310
|
* overview, or N for judge N. open holds the ranks of the judges that have started, count the number of
|
|
210
|
-
* criteria
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
311
|
+
* criteria, maxScroll the lines the detail pane can hide, page the lines one PgUp or PgDn moves. With the
|
|
312
|
+
* focus on a judge, every key but ctrl-q goes to the judge, and ctrl-q moves the focus to the strip. With the
|
|
313
|
+
* focus on the strip, the arrows left and right move the active tab and switch the view, 1-9 jump to a started
|
|
314
|
+
* judge, 0 to the overview, Enter, Esc, or ctrl-q give the focus back to the judge, and q quits (a second q
|
|
315
|
+
* confirms while a judge runs). On the overview, which takes no text, up and down move the cursor, r restarts
|
|
316
|
+
* the judge of the cursor row, h flips its harness, and Enter moves the focus into the detail pane: there up
|
|
317
|
+
* and down scroll one line, PgUp and PgDn a page, and Esc or Enter return the focus to the list.
|
|
215
318
|
*/
|
|
216
|
-
export function press({ view, confirm, focus, cursor }, key, { open, running, count }) {
|
|
217
|
-
const strip = (v) => ({ view: v, confirm: false, focus: 'strip', cursor });
|
|
218
|
-
if (focus === 'judge') return TOGGLE.includes(key) ? strip(view) : { view, confirm, focus, cursor, send: true };
|
|
319
|
+
export function press({ view, confirm, focus, cursor, scroll = 0 }, key, { open, running, count, maxScroll: limit = 0, page = 10 }) {
|
|
320
|
+
const strip = (v) => ({ view: v, confirm: false, focus: 'strip', cursor, scroll });
|
|
321
|
+
if (focus === 'judge') return TOGGLE.includes(key) ? strip(view) : { view, confirm, focus, cursor, scroll, send: true };
|
|
219
322
|
if (confirm) return { ...strip(0), quit: key === 'q' };
|
|
220
|
-
if (key === 'q') return running ? { view: 0, confirm: true, focus: 'strip', cursor } : { ...strip(0), quit: true };
|
|
323
|
+
if (key === 'q') return running ? { view: 0, confirm: true, focus: 'strip', cursor, scroll } : { ...strip(0), quit: true };
|
|
221
324
|
if (RIGHT.includes(key) || LEFT.includes(key)) {
|
|
222
325
|
const cycle = [0, ...open];
|
|
223
326
|
const step = RIGHT.includes(key) ? 1 : -1;
|
|
224
327
|
return strip(cycle[(cycle.indexOf(view) + step + cycle.length) % cycle.length]);
|
|
225
328
|
}
|
|
226
|
-
if (/^[1-9]$/.test(key)) return open.includes(Number(key)) ? strip(Number(key)) : strip(view);
|
|
329
|
+
if (/^[1-9]$/.test(key)) return open.includes(Number(key)) ? strip(Number(key)) : { ...strip(view), focus };
|
|
227
330
|
if (key === '0') return strip(0);
|
|
228
331
|
if (view === 0) {
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
if (key === '
|
|
232
|
-
if (
|
|
332
|
+
const detail = (s) => ({ view: 0, confirm: false, focus: 'detail', cursor, scroll: Math.max(0, Math.min(limit, s)) });
|
|
333
|
+
if (key === 'r') return open.includes(cursor + 1) ? { ...strip(0), focus, restart: cursor + 1 } : { ...strip(0), focus };
|
|
334
|
+
if (key === 'h') return { ...strip(0), focus, flip: cursor + 1 };
|
|
335
|
+
if (focus === 'detail') {
|
|
336
|
+
if (UP.includes(key)) return detail(scroll - 1);
|
|
337
|
+
if (DOWN.includes(key)) return detail(scroll + 1);
|
|
338
|
+
if (key === '\x1b[5~') return detail(scroll - page);
|
|
339
|
+
if (key === '\x1b[6~') return detail(scroll + page);
|
|
340
|
+
if (key === '\r' || key === '\x1b') return strip(0);
|
|
341
|
+
return detail(scroll);
|
|
342
|
+
}
|
|
343
|
+
if (UP.includes(key)) return { ...strip(0), cursor: Math.max(0, cursor - 1), scroll: 0 };
|
|
344
|
+
if (DOWN.includes(key)) return { ...strip(0), cursor: Math.min(count - 1, cursor + 1), scroll: 0 };
|
|
345
|
+
if (key === '\r') return detail(0);
|
|
233
346
|
return strip(0);
|
|
234
347
|
}
|
|
235
|
-
if (BACK.includes(key)) return { view, confirm: false, focus: 'judge', cursor };
|
|
348
|
+
if (BACK.includes(key)) return { view, confirm: false, focus: 'judge', cursor, scroll };
|
|
236
349
|
return strip(view);
|
|
237
350
|
}
|
|
238
351
|
|
|
239
352
|
/** Runs the judges, shows the overview, and writes the report. Resolves when the engineer quits. */
|
|
240
|
-
export function runTable({ panel, runDir, harness, cwd, target, brief, sources }) {
|
|
353
|
+
export function runTable({ panel, runDir, harness, cwd, target, brief, sources, yolo = false }) {
|
|
241
354
|
const out = process.stdout;
|
|
242
355
|
const criteria = panel.criteria.map((c, i) => ({ rank: i + 1, ...c }));
|
|
243
356
|
const judges = [];
|
|
@@ -245,7 +358,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
245
358
|
const harnesses = criteria.map(() => harness);
|
|
246
359
|
let rows = [];
|
|
247
360
|
let final = null;
|
|
248
|
-
let ui = { view: 0, confirm: false, focus: 'strip', cursor: 0 };
|
|
361
|
+
let ui = { view: 0, confirm: false, focus: 'strip', cursor: 0, scroll: 0 };
|
|
249
362
|
let stripTimer;
|
|
250
363
|
let lastData = 0;
|
|
251
364
|
const started = Date.now();
|
|
@@ -282,7 +395,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
282
395
|
}
|
|
283
396
|
|
|
284
397
|
function start(rank, restartedAt = null) {
|
|
285
|
-
const [command, ...argv] = writeJudge({ dir: judgeDir(rank), criterion: criteria[rank - 1], harness: harnesses[rank - 1], cwd, target, brief, sources });
|
|
398
|
+
const [command, ...argv] = writeJudge({ dir: judgeDir(rank), criterion: criteria[rank - 1], harness: harnesses[rank - 1], cwd, target, brief, sources, yolo });
|
|
286
399
|
const { cols, rows: lines } = size();
|
|
287
400
|
const term = new xterm.Terminal({ cols, rows: lines, allowProposedApi: true });
|
|
288
401
|
const serializer = new SerializeAddon();
|
|
@@ -362,7 +475,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
362
475
|
|
|
363
476
|
let frame = 0;
|
|
364
477
|
function draw() {
|
|
365
|
-
const lines = renderOverview({ title: `${panel.owner}/${panel.name} v${panel.version}`, target, harness, elapsed: seconds(started), rows, cursor: ui.cursor, width: out.columns, frame });
|
|
478
|
+
const lines = renderOverview({ title: `${panel.owner}/${panel.name} v${panel.version}`, target, harness, elapsed: seconds(started), rows, cursor: ui.cursor, width: out.columns, height: out.rows - STRIP_ROWS, frame, focus: ui.focus === 'detail' ? 'detail' : 'list', scroll: ui.scroll });
|
|
366
479
|
while (lines.length < out.rows - STRIP_ROWS) lines.push('');
|
|
367
480
|
out.write(lines.slice(0, out.rows - STRIP_ROWS).map((l, i) => `\x1b[${i + 1};1H${l}\x1b[K`).join(''));
|
|
368
481
|
drawStrip();
|
|
@@ -396,8 +509,10 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
396
509
|
process.stdin.on('data', (data) => {
|
|
397
510
|
const key = data.toString();
|
|
398
511
|
const from = ui.view;
|
|
399
|
-
const
|
|
400
|
-
|
|
512
|
+
const height = out.rows - STRIP_ROWS;
|
|
513
|
+
const limit = rows.length ? maxScroll({ rows, cursor: ui.cursor, width: out.columns, height }) : 0;
|
|
514
|
+
const next = press(ui, key, { open: judges.flatMap((j, i) => (j ? [i + 1] : [])), running: rows.some((r) => r.state === 'running'), count: criteria.length, maxScroll: limit, page: Math.max(1, height - 7) });
|
|
515
|
+
ui = { view: next.view, confirm: next.confirm, focus: next.focus, cursor: next.cursor, scroll: next.scroll };
|
|
401
516
|
if (next.send) {
|
|
402
517
|
judges[from - 1].proc.write(key);
|
|
403
518
|
} else if (next.quit) {
|
package/package.json
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsft/panels",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Run a panel of AI judges from cross.software/panels against a target.",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"panels": "bin/panels.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"prompts",
|
|
14
|
+
"schema",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
9
17
|
"license": "UNLICENSED",
|
|
10
|
-
"engines": {
|
|
11
|
-
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.js"
|
|
23
|
+
},
|
|
12
24
|
"dependencies": {
|
|
13
25
|
"@homebridge/node-pty-prebuilt-multiarch": "0.14.1",
|
|
14
26
|
"@xterm/addon-serialize": "0.14.0",
|
|
15
|
-
"@xterm/headless": "6.0.0"
|
|
27
|
+
"@xterm/headless": "6.0.0",
|
|
28
|
+
"figlet": "1.12.0"
|
|
16
29
|
}
|
|
17
30
|
}
|
|
@@ -33,7 +33,7 @@ EVIDENCE
|
|
|
33
33
|
|
|
34
34
|
THE VERDICT
|
|
35
35
|
|
|
36
|
-
One paragraph
|
|
36
|
+
One paragraph: what holds, then what is wrong and where, each place in brackets, then "Not checked:" and the tests you could not do, when there are any. Judge; do not propose fixes. The fixes are the job of the engineer.
|
|
37
37
|
|
|
38
38
|
PROHIBITIONS
|
|
39
39
|
|
|
@@ -51,7 +51,7 @@ Return one JSON object and nothing else:
|
|
|
51
51
|
{
|
|
52
52
|
"name": string, the name of your criterion,
|
|
53
53
|
"score": integer 1 to 4,
|
|
54
|
-
"verdict": string, one paragraph
|
|
54
|
+
"verdict": string, one paragraph
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
Example:
|
package/schema/report.json
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"properties": {
|
|
32
32
|
"name": { "type": "string" },
|
|
33
33
|
"score": { "type": "integer", "minimum": 1, "maximum": 4 },
|
|
34
|
-
"verdict": { "type": "string", "minLength": 1, "
|
|
34
|
+
"verdict": { "type": "string", "minLength": 1, "description": "One paragraph: what holds, what is wrong and where (the place in brackets), and \"Not checked: ...\" at the end when something was not checked." }
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|