@xsft/panels 0.5.1 → 0.6.0
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 +1 -1
- package/lib/judge.js +1 -4
- package/lib/report.js +3 -1
- package/lib/table.js +24 -16
- package/package.json +1 -1
- package/prompts/judge-system-prompt.txt +6 -14
- package/schema/report.json +1 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Run the command in the folder of the product. Each judge starts in that folder,
|
|
|
31
31
|
|
|
32
32
|
On a terminal, `panels run` shows the score table, and each judge runs in its interactive harness in a pseudo-terminal inside the command. Each judge writes `judges/N/answer.json` and stays open. The table writes the report when each judge has an answer or an error, and the command exits from `report.json` when you quit. Without a terminal, the judges run in the background (`claude -p`, `codex exec`).
|
|
33
33
|
|
|
34
|
-
A tab strip on the last three lines shows a hairline, a tab per view, and a status line with the progress, the elapsed time, and the keys. The tabs are the scores, then each judge with its number, name, and mark (score, ✗ for an error, · while queued, ! while the harness asks whether to trust the folder).
|
|
34
|
+
A tab strip on the last three lines shows a hairline, a tab per view, and a status line with the progress, the elapsed time, and the keys. The tabs are the scores, then each judge with its number, name, and mark (score, ✗ for an error, · while queued, ! while the harness asks whether to trust the folder). One soft wave sweeps across the strip and lights the labels of the running judges as it passes; the same wave sweeps the running rows of the scores table. 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, `0` to the scores, `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 scores take no input, 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
35
|
|
|
36
36
|
## Run folder
|
|
37
37
|
|
package/lib/judge.js
CHANGED
|
@@ -9,8 +9,6 @@ const TOOLS = 'Read,Glob,Grep,Bash,WebFetch,WebSearch';
|
|
|
9
9
|
// With --permission-mode dontAsk, Claude refuses every tool call that this list does not allow, so Bash runs git reads only.
|
|
10
10
|
const ALLOWED_TOOLS = 'Read,Glob,Grep,WebFetch,WebSearch,Bash(git log:*),Bash(git show:*),Bash(git diff:*),Bash(git status:*),Bash(git ls-files:*),Bash(git rev-parse:*),Bash(git branch --list:*)';
|
|
11
11
|
|
|
12
|
-
export const bandToScore = (band) => (band <= 3 ? 1 : band <= 6 ? 2 : band <= 8 ? 3 : 4);
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* The user message of a judge. criteria items are { rank, name, description, body }. Each part sits
|
|
16
14
|
* in a tag, because a body is markdown with headings of its own.
|
|
@@ -45,8 +43,7 @@ export function check(answer, criteria) {
|
|
|
45
43
|
if (missing.length) return [`Item ${i + 1} lacks ${missing.join(', ')}.`];
|
|
46
44
|
const errors = [];
|
|
47
45
|
if (r.rank !== c.rank || r.criterion !== c.name) errors.push(`Item ${i + 1} must have rank ${c.rank} and criterion "${c.name}".`);
|
|
48
|
-
if (!Number.isInteger(r.
|
|
49
|
-
else if (r.score !== bandToScore(r.band)) errors.push(`Rank ${c.rank}: band ${r.band} maps to score ${bandToScore(r.band)}, not ${r.score}.`);
|
|
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.`);
|
|
50
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.`);
|
|
51
48
|
return errors;
|
|
52
49
|
});
|
package/lib/report.js
CHANGED
|
@@ -15,6 +15,8 @@ export function buildReport({ panel, target, cwd, harness, judges, outcomes }) {
|
|
|
15
15
|
return { status, panel: { owner: panel.owner, name: panel.name, version: panel.version }, target, cwd, harness, size: judges.length, criteria, errors };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const LEVELS = { 1: 'Fails', 2: 'Falls short', 3: 'Almost met', 4: 'Met' };
|
|
19
|
+
|
|
18
20
|
const cell = (text) => text.replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
19
21
|
|
|
20
22
|
export function renderMarkdown(report) {
|
|
@@ -32,7 +34,7 @@ export function renderMarkdown(report) {
|
|
|
32
34
|
const list = (title, items) => items.length && lines.push(title, ...items.map((item) => `- ${item}`));
|
|
33
35
|
for (const c of criteria) {
|
|
34
36
|
for (const { result } of c.judges) {
|
|
35
|
-
lines.push('', `## ${c.rank}. ${c.name}: ${result.score}
|
|
37
|
+
lines.push('', `## ${c.rank}. ${c.name}: ${result.score}/4 ${LEVELS[result.score]}`);
|
|
36
38
|
list('Evidence', result.evidence.map((e) => `${e.where}: ${e.quote ?? e.claim}`));
|
|
37
39
|
list('Fixes', result.fixes.map((f) => `${f.problem} ${f.change} (${f.where})`));
|
|
38
40
|
list('Not checked', result.notChecked.map((n) => `${n.test}: ${n.reason}`));
|
package/lib/table.js
CHANGED
|
@@ -31,16 +31,21 @@ const TABLE_MODES = '\x1b[?1049h\x1b[r\x1b[0m\x1b[4l\x1b[?6l\x1b[?7h\x1b[?1l\x1b
|
|
|
31
31
|
* trust is true while the harness of a running judge asks whether to trust the folder.
|
|
32
32
|
* final is null until the report exists, then { status, path }. confirm is true while q waits for y.
|
|
33
33
|
*/
|
|
34
|
-
// The glimmer on the text of
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const
|
|
34
|
+
// The glimmer on the text of the running judges: grey text, and one soft wave three columns wide that moves one
|
|
35
|
+
// column per frame from the left edge of the screen to the right edge and starts over. A running label lights
|
|
36
|
+
// up only where the wave passes over its own columns, so every running label lights at the same columns.
|
|
37
|
+
const GLIMMER = { base: '\x1b[38;5;244m', mid: '\x1b[38;5;247m', high: '\x1b[38;5;250m' };
|
|
38
|
+
const GLIMMER_ORANGE = { base: ORANGE, mid: '\x1b[38;2;255;140;100m', high: '\x1b[38;2;255;175;145m' };
|
|
39
|
+
const WAVE = 3;
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* text with the glimmer at frame, where the text starts at the screen column start (0-based) on a screen width
|
|
43
|
+
* columns wide. colours is { base, mid, high }; prefix (such as bold) goes before each piece.
|
|
44
|
+
*/
|
|
45
|
+
export function glimmer(text, frame, start, width, colours = GLIMMER, prefix = '') {
|
|
46
|
+
const centre = (frame % (width + WAVE)) - 1;
|
|
42
47
|
return [...text].map((c, i) => {
|
|
43
|
-
const d = Math.
|
|
48
|
+
const d = Math.abs(start + i - centre);
|
|
44
49
|
return `${prefix}${d === 0 ? colours.high : d === 1 ? colours.mid : colours.base}${c}`;
|
|
45
50
|
}).join('') + RESET;
|
|
46
51
|
}
|
|
@@ -54,17 +59,17 @@ export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
|
54
59
|
for (const r of rows) {
|
|
55
60
|
const head = `${String(r.rank).padStart(3)} ${r.name.padEnd(pad)}`;
|
|
56
61
|
if (r.state === 'running' && r.trust) lines.push(`${ORANGE}${head}press ${r.rank} and answer the trust prompt${RESET}`);
|
|
57
|
-
else if (r.state === 'running') lines.push(`${String(r.rank).padStart(3)} ${glimmer(r.name, frame)}`);
|
|
62
|
+
else if (r.state === 'running') lines.push(`${String(r.rank).padStart(3)} ${glimmer(r.name, frame, 5, width)}`);
|
|
58
63
|
else if (r.state === 'queued') lines.push(`${head}${GREY}queued${RESET}`);
|
|
59
64
|
else if (r.state === 'error') lines.push(`${head}error`);
|
|
60
|
-
else lines.push(`${head}${r.result.score}
|
|
65
|
+
else lines.push(`${head}${r.result.score}/4`);
|
|
61
66
|
}
|
|
62
67
|
lines.push(rule);
|
|
63
68
|
if (final) {
|
|
64
69
|
const at4 = rows.filter((r) => r.state === 'done' && r.result.score === 4).length;
|
|
65
70
|
lines.push(` ${BOLD}${final.status.toUpperCase()}${RESET} ${at4} of ${rows.length} at 4`, ` report ${final.path}`);
|
|
66
71
|
}
|
|
67
|
-
if (confirm) lines.push(' judges still run.
|
|
72
|
+
if (confirm) lines.push(' judges still run. q again quits, another key stays');
|
|
68
73
|
return lines;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -81,7 +86,7 @@ export function renderStrip({ rows, view, final, frame = 0, width, focus = 'stri
|
|
|
81
86
|
if (r.state === 'running') return r.trust ? '!' : '';
|
|
82
87
|
if (r.state === 'queued') return '·';
|
|
83
88
|
if (r.state === 'error') return '✗';
|
|
84
|
-
return
|
|
89
|
+
return `${r.result.score}/4`;
|
|
85
90
|
};
|
|
86
91
|
const finished = rows.filter((r) => r.state === 'done' || r.state === 'error').length;
|
|
87
92
|
const clock = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, '0')}`;
|
|
@@ -94,14 +99,17 @@ export function renderStrip({ rows, view, final, frame = 0, width, focus = 'stri
|
|
|
94
99
|
const used = (list) => 1 + list.reduce((n, t) => n + plain(t).length, 0) + 2 * (list.length - 1);
|
|
95
100
|
const list = [0, 1, 2].map(tabs).find((l) => used(l) <= width) ?? tabs(2);
|
|
96
101
|
let line = ' ';
|
|
102
|
+
let col = 1;
|
|
97
103
|
list.forEach((t, i) => {
|
|
98
|
-
if (i) line += ' ';
|
|
104
|
+
if (i) { line += ' '; col += 2; }
|
|
99
105
|
const active = i === view;
|
|
100
106
|
const base = active ? (focus === 'strip' ? `${ORANGE}${BOLD}` : BOLD) : GREY;
|
|
101
107
|
const text = plain(t);
|
|
102
108
|
const row = rows[i - 1];
|
|
109
|
+
const start = col;
|
|
110
|
+
col += text.length;
|
|
103
111
|
if (row?.state === 'running' && !row.trust) {
|
|
104
|
-
line += glimmer(text, frame, active && focus === 'strip' ? GLIMMER_ORANGE : GLIMMER, active ? BOLD : '');
|
|
112
|
+
line += glimmer(text, frame, start, width, active && focus === 'strip' ? GLIMMER_ORANGE : GLIMMER, active ? BOLD : '');
|
|
105
113
|
return;
|
|
106
114
|
}
|
|
107
115
|
const pieces = [];
|
|
@@ -120,13 +128,13 @@ export function renderStrip({ rows, view, final, frame = 0, width, focus = 'stri
|
|
|
120
128
|
* view is 0 for the scores, or N for judge N. open holds the ranks of the judges that have started. With the
|
|
121
129
|
* focus on a judge, every key but ctrl-q goes to the judge, and ctrl-q moves the focus to the strip. With the
|
|
122
130
|
* focus on the strip, the arrows move the active tab and switch the view, 1-9 jump to a started judge, 0 to the
|
|
123
|
-
* scores, Enter, Esc, or ctrl-q give the focus back to the judge, and q quits (
|
|
131
|
+
* scores, Enter, Esc, or ctrl-q give the focus back to the judge, and q quits (a second q confirms while a judge runs).
|
|
124
132
|
* The scores take no input, so the strip keeps the focus there.
|
|
125
133
|
*/
|
|
126
134
|
export function press({ view, confirm, focus }, key, { open, running }) {
|
|
127
135
|
const strip = (v) => ({ view: v, confirm: false, focus: 'strip' });
|
|
128
136
|
if (focus === 'judge') return TOGGLE.includes(key) ? strip(view) : { view, confirm, focus, send: true };
|
|
129
|
-
if (confirm) return { ...strip(0), quit: key === '
|
|
137
|
+
if (confirm) return { ...strip(0), quit: key === 'q' };
|
|
130
138
|
if (key === 'q') return running ? { view: 0, confirm: true, focus: 'strip' } : { ...strip(0), quit: true };
|
|
131
139
|
if (RIGHT.includes(key) || LEFT.includes(key)) {
|
|
132
140
|
const cycle = [0, ...open];
|
package/package.json
CHANGED
|
@@ -20,22 +20,16 @@ Score your criteria one at a time, in rank order. Before you read the brief or t
|
|
|
20
20
|
|
|
21
21
|
HOW TO SCORE
|
|
22
22
|
|
|
23
|
-
1. Follow the body to a level from 1 to
|
|
24
|
-
2. Apply each cap of the body, such as "cannot support a score above
|
|
25
|
-
3. When the evidence fits two
|
|
26
|
-
4.
|
|
27
|
-
band 1 to 3 = score 1
|
|
28
|
-
band 4 to 6 = score 2
|
|
29
|
-
band 7 or 8 = score 3
|
|
30
|
-
band 9 or 10 = score 4
|
|
31
|
-
5. A criterion passes only at score 4.
|
|
23
|
+
1. Follow the body to a level from 1 to 4: 4 Met, 3 Almost met, 2 Falls short, 1 Fails. The level is the score.
|
|
24
|
+
2. Apply each cap of the body, such as "cannot support a score above 3". When two caps apply, use the lower cap.
|
|
25
|
+
3. When the evidence fits two levels, use the lower level.
|
|
26
|
+
4. A criterion passes only at score 4.
|
|
32
27
|
|
|
33
28
|
EVIDENCE
|
|
34
29
|
|
|
35
30
|
- Each claim needs evidence that the engineer can check: a URL, a file and line, a quote, or a command and its output.
|
|
36
31
|
- Give no credit for intent. A promise in a comment, a doc, or the brief is not done work.
|
|
37
|
-
- No evidence for a
|
|
38
|
-
- At band 9, name the one specific gap of the body in the evidence.
|
|
32
|
+
- No evidence for a level means the lower level.
|
|
39
33
|
|
|
40
34
|
RECORDS AND FIXES
|
|
41
35
|
|
|
@@ -44,7 +38,7 @@ RECORDS AND FIXES
|
|
|
44
38
|
|
|
45
39
|
PROHIBITIONS
|
|
46
40
|
|
|
47
|
-
- Do not change the body, the
|
|
41
|
+
- Do not change the body, the levels, or the caps.
|
|
48
42
|
- Do not score a fault that the body gives to another criterion.
|
|
49
43
|
- Do not write or change files. Name each fix, and do not make it.
|
|
50
44
|
- Do not submit forms, send messages, or post data.
|
|
@@ -58,7 +52,6 @@ Return one JSON object and nothing else. Give one item in results for each of yo
|
|
|
58
52
|
"results": [{
|
|
59
53
|
"rank": integer,
|
|
60
54
|
"criterion": string,
|
|
61
|
-
"band": integer 1 to 10,
|
|
62
55
|
"score": integer 1 to 4,
|
|
63
56
|
"verdict": string, one sentence of 30 words or less,
|
|
64
57
|
"records": [{ "name": string, "columns": [string], "rows": [[string]] }],
|
|
@@ -73,7 +66,6 @@ Example of one item:
|
|
|
73
66
|
{
|
|
74
67
|
"rank": 5,
|
|
75
68
|
"criterion": "Original design assets",
|
|
76
|
-
"band": 5,
|
|
77
69
|
"score": 2,
|
|
78
70
|
"verdict": "The hero shows the real app, and the six feature icons are Lucide icons.",
|
|
79
71
|
"records": [{
|
package/schema/report.json
CHANGED
|
@@ -61,11 +61,10 @@
|
|
|
61
61
|
"judgeResult": {
|
|
62
62
|
"type": "object",
|
|
63
63
|
"additionalProperties": false,
|
|
64
|
-
"required": ["rank", "criterion", "
|
|
64
|
+
"required": ["rank", "criterion", "score", "verdict", "records", "evidence", "notChecked", "fixes"],
|
|
65
65
|
"properties": {
|
|
66
66
|
"rank": { "type": "integer" },
|
|
67
67
|
"criterion": { "type": "string" },
|
|
68
|
-
"band": { "type": "integer", "minimum": 1, "maximum": 10 },
|
|
69
68
|
"score": { "type": "integer", "minimum": 1, "maximum": 4 },
|
|
70
69
|
"verdict": { "type": "string" },
|
|
71
70
|
"records": {
|