@xsft/panels 0.6.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 +8 -4
- package/bin/panels.js +13 -10
- package/lib/args.js +5 -1
- package/lib/judge.js +42 -57
- package/lib/report.js +16 -37
- package/lib/table.js +336 -108
- package/package.json +19 -6
- package/prompts/judge-system-prompt.txt +16 -36
- package/schema/report.json +11 -97
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,16 +31,18 @@ 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
|
|
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
|
-
|
|
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.
|
|
37
|
+
|
|
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.
|
|
35
39
|
|
|
36
40
|
## Run folder
|
|
37
41
|
|
|
38
42
|
Each run writes to `.panels/<owner>-<name>/<UTC time>/`:
|
|
39
43
|
|
|
40
44
|
- `panel.json`: the panel as the api gave it.
|
|
41
|
-
- `report.
|
|
45
|
+
- `report.json`: `{ panel: "owner/name@version", target, status, criteria: [{ name, score, verdict }] }`, in panel order. `schema/report.json` is its schema, and its `criterion` definition is the shape of the answer of a judge.
|
|
42
46
|
- `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.
|
|
43
47
|
|
|
44
48
|
The last line on stdout is the path of `report.json`.
|
package/bin/panels.js
CHANGED
|
@@ -4,7 +4,7 @@ import { delimiter, join } from 'node:path';
|
|
|
4
4
|
import { parse, SYNOPSIS } from '../lib/args.js';
|
|
5
5
|
import { runJudge } from '../lib/judge.js';
|
|
6
6
|
import { fetchPanel } from '../lib/panel.js';
|
|
7
|
-
import { buildReport,
|
|
7
|
+
import { buildReport, renderPlain } from '../lib/report.js';
|
|
8
8
|
import { runTable } from '../lib/table.js';
|
|
9
9
|
|
|
10
10
|
// More parallel sessions on one account get 429 answers from the model api.
|
|
@@ -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}`);
|
|
@@ -62,27 +67,25 @@ async function main() {
|
|
|
62
67
|
}
|
|
63
68
|
const { panel } = fetched;
|
|
64
69
|
const cwd = process.cwd();
|
|
65
|
-
const judges = panel.criteria.map((_, i) => [i]);
|
|
66
70
|
const time = new Date().toISOString().replaceAll(':', '-');
|
|
67
71
|
const runDir = join(cwd, '.panels', `${panel.owner}-${panel.name}`, time);
|
|
68
72
|
mkdirSync(runDir, { recursive: true });
|
|
69
73
|
writeFileSync(join(runDir, 'panel.json'), JSON.stringify(panel, null, 2));
|
|
70
74
|
if (process.stdout.isTTY && process.stdin.isTTY) return runOnTerminal({ panel, runDir, harness, cwd, args });
|
|
71
|
-
const outcomes = await pool(
|
|
72
|
-
dir: join(runDir, 'judges', String(
|
|
73
|
-
|
|
75
|
+
const outcomes = await pool(panel.criteria, PARALLEL_JUDGES, (c, i) => runJudge({
|
|
76
|
+
dir: join(runDir, 'judges', String(i + 1)),
|
|
77
|
+
criterion: { rank: i + 1, ...c },
|
|
74
78
|
harness,
|
|
75
79
|
cwd,
|
|
76
80
|
target: args.target,
|
|
77
81
|
brief: args.brief,
|
|
78
82
|
sources: args.sources,
|
|
83
|
+
yolo: true,
|
|
79
84
|
}));
|
|
80
|
-
const report = buildReport({ panel, target: args.target,
|
|
81
|
-
const markdown = renderMarkdown(report);
|
|
85
|
+
const report = buildReport({ panel, target: args.target, outcomes });
|
|
82
86
|
const reportPath = join(runDir, 'report.json');
|
|
83
|
-
writeFileSync(join(runDir, 'report.md'), markdown);
|
|
84
87
|
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
85
|
-
console.log(
|
|
88
|
+
console.log(renderPlain(report).join('\n'));
|
|
86
89
|
console.log(reportPath);
|
|
87
90
|
return EXIT[report.status];
|
|
88
91
|
}
|
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
|
@@ -2,51 +2,43 @@ 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
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { criterion: CRITERION } = JSON.parse(readFileSync(new URL('../schema/report.json', import.meta.url), 'utf8')).$defs;
|
|
6
6
|
const SYSTEM_PROMPT = readFileSync(new URL('../prompts/judge-system-prompt.txt', import.meta.url), 'utf8');
|
|
7
7
|
export const MINUTES_PER_CRITERION = 20;
|
|
8
|
-
|
|
9
|
-
// With --
|
|
10
|
-
|
|
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"'];
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
|
-
* The user message of a judge.
|
|
14
|
-
*
|
|
17
|
+
* The user message of a judge. criterion is { rank, name, description, body }. Each part sits in a tag,
|
|
18
|
+
* because a body is markdown with headings of its own.
|
|
15
19
|
*/
|
|
16
|
-
export function userMessage(
|
|
17
|
-
const parts =
|
|
20
|
+
export function userMessage(criterion, { target, brief, sources }) {
|
|
21
|
+
const parts = [`<criterion rank="${criterion.rank}" name="${criterion.name}">\nDescription: ${criterion.description}\n\n${criterion.body}\n</criterion>`];
|
|
18
22
|
parts.push(`<target>\n${target}\n</target>`, `<brief>\n${brief ?? 'none supplied'}\n</brief>`, `<sources>\n${sources.length ? sources.join('\n') : 'none'}\n</sources>`);
|
|
19
23
|
return `${parts.join('\n\n')}\n`;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
/** The output schema of a judge:
|
|
23
|
-
export function judgeSchema(
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
properties: { ...judgeResult.properties, rank: { type: 'integer', const: c.rank }, criterion: { type: 'string', const: c.name } },
|
|
27
|
-
}));
|
|
28
|
-
return {
|
|
29
|
-
type: 'object',
|
|
30
|
-
additionalProperties: false,
|
|
31
|
-
required: ['results'],
|
|
32
|
-
properties: { results: { type: 'array', minItems: items.length, maxItems: items.length, items: items.length === 1 ? items[0] : { anyOf: items } } },
|
|
33
|
-
};
|
|
26
|
+
/** The output schema of a judge: one criterion object { name, score, verdict }, with the name fixed. */
|
|
27
|
+
export function judgeSchema(criterion) {
|
|
28
|
+
const { description, ...schema } = CRITERION;
|
|
29
|
+
return { ...schema, properties: { ...schema.properties, name: { type: 'string', const: criterion.name } } };
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/** Returns the reasons the answer of a judge is not valid, or an empty array. */
|
|
37
|
-
export function check(answer,
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!Number.isInteger(r.score) || r.score < 1 || r.score > 4) errors.push(`Rank ${c.rank}: score must be a whole number from 1 to 4.`);
|
|
47
|
-
if (r.score < 4 && !(Array.isArray(r.fixes) && r.fixes.length)) errors.push(`Rank ${c.rank}: score ${r.score} is below 4, so fixes must list each fix that score 4 needs.`);
|
|
48
|
-
return errors;
|
|
49
|
-
});
|
|
33
|
+
export function check(answer, criterion) {
|
|
34
|
+
if (answer === null || typeof answer !== 'object' || Array.isArray(answer)) return ['The answer must be one JSON object with name, score, and verdict.'];
|
|
35
|
+
const missing = CRITERION.required.filter((key) => !(key in answer));
|
|
36
|
+
if (missing.length) return [`The answer lacks ${missing.join(', ')}.`];
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (answer.name !== criterion.name) errors.push(`name must be "${criterion.name}".`);
|
|
39
|
+
if (!Number.isInteger(answer.score) || answer.score < 1 || answer.score > 4) errors.push('score must be a whole number from 1 to 4.');
|
|
40
|
+
if (typeof answer.verdict !== 'string' || !answer.verdict.trim()) errors.push('verdict must be one paragraph of text.');
|
|
41
|
+
return errors;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
/** The folders of the brief and the path sources that are outside cwd. A Claude judge cannot read them without --add-dir. */
|
|
@@ -72,10 +64,7 @@ function exec(command, args, { cwd, stdin, timeout }) {
|
|
|
72
64
|
|
|
73
65
|
async function askClaude(j, prompt, session) {
|
|
74
66
|
const args = [
|
|
75
|
-
'-p', '
|
|
76
|
-
'--tools', TOOLS,
|
|
77
|
-
'--allowedTools', ALLOWED_TOOLS,
|
|
78
|
-
'--permission-mode', 'dontAsk', '--permission-prompts', 'none',
|
|
67
|
+
'-p', ...yoloFlags(j.yolo, 'claude'),
|
|
79
68
|
...j.addDirs.flatMap((d) => ['--add-dir', d]),
|
|
80
69
|
'--system-prompt', SYSTEM_PROMPT,
|
|
81
70
|
'--json-schema', JSON.stringify(j.schema),
|
|
@@ -92,15 +81,13 @@ async function askClaude(j, prompt, session) {
|
|
|
92
81
|
return { answer: result.structured_output, session: result.session_id };
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
// --disable apps: the apps feature gives Codex the connectors of the ChatGPT account, and some of them write
|
|
96
|
-
// (chatgpt_space_create_page, chatgpt_space_trash_page). --ignore-user-config does not turn them off.
|
|
97
84
|
async function askCodex(j, prompt, thread) {
|
|
98
85
|
const raw = join(j.dir, 'raw.json');
|
|
99
86
|
const instructions = `developer_instructions=${JSON.stringify(SYSTEM_PROMPT)}`;
|
|
100
87
|
const output = ['--output-schema', join(j.dir, 'schema.json'), '-o', raw, '--json'];
|
|
101
88
|
const args = thread
|
|
102
|
-
? ['exec', 'resume', thread, prompt, '--skip-git-repo-check', '--ignore-user-config',
|
|
103
|
-
: ['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];
|
|
104
91
|
const stdin = thread ? 'ignore' : openSync(join(j.dir, 'prompt.md'));
|
|
105
92
|
const run = await exec(j.command, args, { cwd: j.cwd, stdin, timeout: j.timeout });
|
|
106
93
|
if (!thread) closeSync(stdin);
|
|
@@ -120,25 +107,25 @@ export function parseAnswer(text) {
|
|
|
120
107
|
|
|
121
108
|
/**
|
|
122
109
|
* Runs one judge to the end: the first answer, and one re-ask when check() rejects it.
|
|
123
|
-
* Returns {
|
|
110
|
+
* Returns { answer } or { error }. command is the harness binary; the tests pass a stub.
|
|
124
111
|
*/
|
|
125
|
-
export async function runJudge({ dir,
|
|
112
|
+
export async function runJudge({ dir, criterion, harness, cwd, target, brief, sources, yolo = false }, command = harness) {
|
|
126
113
|
mkdirSync(dir, { recursive: true });
|
|
127
|
-
const prompt = userMessage(
|
|
128
|
-
const schema = judgeSchema(
|
|
114
|
+
const prompt = userMessage(criterion, { target, brief, sources });
|
|
115
|
+
const schema = judgeSchema(criterion);
|
|
129
116
|
writeFileSync(join(dir, 'prompt.md'), prompt);
|
|
130
117
|
writeFileSync(join(dir, 'schema.json'), JSON.stringify(schema, null, 2));
|
|
131
|
-
const j = { dir, cwd, schema, command, addDirs: addDirs(cwd, brief, sources), timeout:
|
|
118
|
+
const j = { dir, cwd, schema, command, yolo, addDirs: addDirs(cwd, brief, sources), timeout: MINUTES_PER_CRITERION * 60_000 };
|
|
132
119
|
const ask = harness === 'claude' ? askClaude : askCodex;
|
|
133
120
|
let reply = await ask(j, prompt);
|
|
134
121
|
if (reply.error) return reply;
|
|
135
|
-
let errors = check(reply.answer,
|
|
122
|
+
let errors = check(reply.answer, criterion);
|
|
136
123
|
if (errors.length) {
|
|
137
124
|
reply = await ask(j, `Your answer failed these checks. Return the whole JSON object again, with each one fixed:\n${errors.map((e) => `- ${e}`).join('\n')}`, reply.session);
|
|
138
125
|
if (reply.error) return reply;
|
|
139
|
-
errors = check(reply.answer,
|
|
126
|
+
errors = check(reply.answer, criterion);
|
|
140
127
|
}
|
|
141
|
-
return errors.length ? { error: `invalid answer after one re-ask: ${errors.join(' ')}` } : {
|
|
128
|
+
return errors.length ? { error: `invalid answer after one re-ask: ${errors.join(' ')}` } : { answer: reply.answer };
|
|
142
129
|
}
|
|
143
130
|
|
|
144
131
|
const TOML_ESCAPES = { '"': '\\"', '\\': '\\\\', '\n': '\\n' };
|
|
@@ -150,19 +137,17 @@ export const tomlString = (s) => `"${s.replace(/["\\\x00-\x1f\x7f]/g, (c) => TOM
|
|
|
150
137
|
* Writes prompt.md and schema.json of one judge of the terminal mode, and returns the command and the
|
|
151
138
|
* arguments that start the judge in its interactive harness.
|
|
152
139
|
*/
|
|
153
|
-
export function writeJudge({ dir,
|
|
140
|
+
export function writeJudge({ dir, criterion, harness, cwd, target, brief, sources, yolo = false }) {
|
|
154
141
|
mkdirSync(dir, { recursive: true });
|
|
155
|
-
const closing = `The engineer ran the command in ${cwd}. As your last step, write your answer JSON,
|
|
156
|
-
const prompt = `${userMessage(
|
|
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`;
|
|
143
|
+
const prompt = `${userMessage(criterion, { target, brief, sources })}\n${closing}`;
|
|
157
144
|
writeFileSync(join(dir, 'prompt.md'), prompt);
|
|
158
|
-
writeFileSync(join(dir, 'schema.json'), JSON.stringify(judgeSchema(
|
|
145
|
+
writeFileSync(join(dir, 'schema.json'), JSON.stringify(judgeSchema(criterion), null, 2));
|
|
159
146
|
if (harness === 'codex') {
|
|
160
147
|
// Codex ranks the developer instructions above the prompt, and the system prompt says to return the JSON and to
|
|
161
148
|
// write no files. Without the closing in the developer instructions, the judge prints the JSON and writes no answer.json.
|
|
162
|
-
return ['codex', '-C', dir,
|
|
149
|
+
return ['codex', '-C', dir, ...yoloFlags(yolo, 'codex'), '-c', `developer_instructions=${tomlString(`${SYSTEM_PROMPT}\n${closing}`)}`, prompt];
|
|
163
150
|
}
|
|
164
|
-
// Claude matches file paths only in Edit rules, which cover Write too; // starts an absolute path.
|
|
165
|
-
const allowed = `${ALLOWED_TOOLS},Edit(/${join(dir, 'answer.json')})`;
|
|
166
151
|
const add = addDirs(cwd, brief, sources).flatMap((d) => ['--add-dir', d]);
|
|
167
|
-
return ['claude', '
|
|
152
|
+
return ['claude', ...yoloFlags(yolo, 'claude'), '--system-prompt', SYSTEM_PROMPT, ...add, prompt];
|
|
168
153
|
}
|
package/lib/report.js
CHANGED
|
@@ -1,45 +1,24 @@
|
|
|
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
|
-
* Builds report.json
|
|
3
|
-
* {
|
|
4
|
-
*
|
|
7
|
+
* Builds report.json: { panel: "owner/name@version", target, status, criteria }. outcomes holds the
|
|
8
|
+
* { answer } or { error } of the judge of each criterion, in panel order. A criterion whose judge failed
|
|
9
|
+
* gets score null and the error as its verdict, and the run status is error. The panel passes only when
|
|
10
|
+
* every criterion scores 4.
|
|
5
11
|
*/
|
|
6
|
-
export function buildReport({ panel, target,
|
|
7
|
-
const errors = outcomes.flatMap((o, j) => (o.error ? [{ judge: j + 1, message: o.error }] : []));
|
|
8
|
-
const failed = new Set(errors.flatMap((e) => judges[e.judge - 1]));
|
|
9
|
-
const answers = judges.flatMap((indexes, j) => (outcomes[j].results ?? []).map((result, x) => ({ i: indexes[x], judge: j + 1, result })));
|
|
12
|
+
export function buildReport({ panel, target, outcomes }) {
|
|
10
13
|
const criteria = panel.criteria.map((c, i) => {
|
|
11
|
-
const
|
|
12
|
-
return
|
|
14
|
+
const { answer, error } = outcomes[i];
|
|
15
|
+
return answer ? { name: c.name, score: answer.score, verdict: answer.verdict } : { name: c.name, score: null, verdict: `error: ${error}` };
|
|
13
16
|
});
|
|
14
17
|
const status = criteria.some((c) => c.score === null) ? 'error' : criteria.every((c) => c.score === 4) ? 'pass' : 'fail';
|
|
15
|
-
return {
|
|
18
|
+
return { panel: `${panel.owner}/${panel.name}@${panel.version}`, target, status, criteria };
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export function renderMarkdown(report) {
|
|
23
|
-
const { status, panel, target, criteria, errors } = report;
|
|
24
|
-
const lines = [
|
|
25
|
-
`${status.toUpperCase()} ${panel.owner}/${panel.name} v${panel.version} on ${JSON.stringify(target)} (${criteria.filter((c) => c.score === 4).length} of ${criteria.length} criteria at 4)`,
|
|
26
|
-
'',
|
|
27
|
-
'| # | Criterion | Score | Verdict |',
|
|
28
|
-
'|---|---|---|---|',
|
|
29
|
-
];
|
|
30
|
-
for (const c of criteria) {
|
|
31
|
-
const lowest = c.judges.find((a) => a.result.score === c.score);
|
|
32
|
-
lines.push(`| ${c.rank} | ${cell(c.name)} | ${c.score ?? 'error'} | ${cell(lowest?.result.verdict ?? '')} |`);
|
|
33
|
-
}
|
|
34
|
-
const list = (title, items) => items.length && lines.push(title, ...items.map((item) => `- ${item}`));
|
|
35
|
-
for (const c of criteria) {
|
|
36
|
-
for (const { result } of c.judges) {
|
|
37
|
-
lines.push('', `## ${c.rank}. ${c.name}: ${result.score}/4 ${LEVELS[result.score]}`);
|
|
38
|
-
list('Evidence', result.evidence.map((e) => `${e.where}: ${e.quote ?? e.claim}`));
|
|
39
|
-
list('Fixes', result.fixes.map((f) => `${f.problem} ${f.change} (${f.where})`));
|
|
40
|
-
list('Not checked', result.notChecked.map((n) => `${n.test}: ${n.reason}`));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (errors.length) lines.push('', '## Errors', ...errors.map((e) => `- judge ${e.judge}: ${e.message}`));
|
|
44
|
-
return `${lines.join('\n')}\n`;
|
|
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. */
|
|
22
|
+
export function renderPlain(report) {
|
|
23
|
+
return [`panels ${VERSION} ${report.panel} ${report.target}`, '', ...report.criteria.flatMap((c) => [`${c.score === null ? 'err' : `${c.score}/4`} ${c.name}`, ` ${c.verdict}`, ''])];
|
|
45
24
|
}
|