atris 3.74.23 → 3.74.25
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 +33 -0
- package/bin/atris.js +10 -0
- package/commands/gm.js +5 -3
- package/commands/rating.js +152 -0
- package/commands/scorecard.js +297 -0
- package/lib/atris-rating-combine.json +77 -0
- package/lib/atris-rating.js +179 -0
- package/lib/gm-bet.js +2 -2
- package/lib/gm-season.js +5 -3
- package/lib/known-commands.js +1 -1
- package/lib/repo-scorecard.js +1135 -0
- package/lib/team-lineup.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -442,6 +442,39 @@ Under the hood, Atris can keep score on real repo work.
|
|
|
442
442
|
- `atris autopilot` can run that check after review and record the result in the journal.
|
|
443
443
|
- Future task picks can use recent results, so Atris learns from repo-local history without claiming model retraining.
|
|
444
444
|
|
|
445
|
+
## Which AI Can Do Your Work
|
|
446
|
+
|
|
447
|
+
`atris scorecard` measures which AI can do your team's work, on your own repo.
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
atris scorecard # find past changes whose own tests can grade an AI (no AI runs)
|
|
451
|
+
atris scorecard run # try each one with Haiku on your Claude plan
|
|
452
|
+
atris scorecard run --models haiku,opus # compare models; past Haiku needs ATRIS_CLAUDE_SPEND_OK=<model>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- A past change becomes a task when its tests fail before it and pass after it. The ask is what your team wrote for that change.
|
|
456
|
+
- The ask also names what the change's tests call that the change added or changed: a new file, a function with its arguments, an export, found with no AI. A good ticket names these; a commit message rarely does, and no model can guess a name nobody wrote down.
|
|
457
|
+
- Each try starts from the change's parent in a throwaway copy with no remote. The change's own tests stay hidden until they grade the try.
|
|
458
|
+
- Every try runs on your Claude plan login. Pay-per-use keys are removed, a try stops the moment Claude Code reports any other billing source, and nothing starts with extra usage on unless you pass `--allow-extra-usage`.
|
|
459
|
+
- The scorecard shows how many changes each model did, how long it took, what the same runs would cost on pay-per-use, and which changes no model could do from what was written.
|
|
460
|
+
- Tests that need their own command or settings go in `.atris/scorecard.json`.
|
|
461
|
+
|
|
462
|
+
## The Atris Rating
|
|
463
|
+
|
|
464
|
+
`atris rating` gives every AI teammate one number, 25 to 99, for how much real work it finishes.
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
atris rating # every engine the combine rated, and your members here
|
|
468
|
+
atris rating opus # one engine's card
|
|
469
|
+
atris rating <member> # one member's rating here, next to its engine's
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
- 99 means 99 of every 100 finished. Each halving of misses is worth about 6 points, so the top is the hardest climb: 3 in a row rates 76, and it takes about 60 in a row to rate 99.
|
|
473
|
+
- Engines are rated in the combine: Loop Bench tasks from the atris repos, each graded by the tests that shipped with it. No engine is a 99 yet.
|
|
474
|
+
- Each engine's card also rates its Python tasks, its JavaScript tasks, the tasks the Atris prompt was never tuned on, and how rarely a try broke another test, with its median time and pay-per-use price per finished task.
|
|
475
|
+
- Members are rated on the same scale from the jobs they landed in your workspace over the last 30 days. Landed means accepted or merged; drops and abandons count against them. `atris gm` shows the same numbers.
|
|
476
|
+
- The session boot shows the build engine's rating, for example `build opus 5.5 (86 ovr)`. The combine grades building only, so review and search engines show none.
|
|
477
|
+
|
|
445
478
|
## Benchmark Harness
|
|
446
479
|
|
|
447
480
|
Atris ships one public head-to-head benchmark harness for comparing a pinned
|
package/bin/atris.js
CHANGED
|
@@ -651,6 +651,8 @@ function showHelpAll() {
|
|
|
651
651
|
console.log(' meet - onboard a stranger in one sitting and print their /book link');
|
|
652
652
|
console.log(' avail - Booking availability (/book/{username} weekly windows)');
|
|
653
653
|
console.log(' scoreboard - Today\'s github contributions vs your best day, streak, repo commits (--watch, --json)');
|
|
654
|
+
console.log(' scorecard - Which AI can do your team\'s work: past changes graded by their own tests, run on your Claude plan (find, run, show)');
|
|
655
|
+
console.log(' rating - The Atris Rating, 25 to 99: how much real work each engine and member finishes (atris rating opus)');
|
|
654
656
|
console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
|
|
655
657
|
console.log(' lesson - atris lesson add <slug> <pass|fail> "<text>" (mine/sweep/resolve/revert/ledger; --detector validates the falsifier at write time)');
|
|
656
658
|
console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
|
|
@@ -2234,6 +2236,14 @@ if (command === 'guide') {
|
|
|
2234
2236
|
Promise.resolve().then(() => require('../commands/scoreboard').scoreboardCommand(process.argv.slice(3)))
|
|
2235
2237
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2236
2238
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2239
|
+
} else if (command === 'scorecard') {
|
|
2240
|
+
Promise.resolve().then(() => require('../commands/scorecard').scorecardCommand(process.argv.slice(3)))
|
|
2241
|
+
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2242
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2243
|
+
} else if (command === 'rating') {
|
|
2244
|
+
Promise.resolve().then(() => require('../commands/rating').ratingCommand(process.argv.slice(3)))
|
|
2245
|
+
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2246
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2237
2247
|
} else if (command === 'meet') {
|
|
2238
2248
|
Promise.resolve().then(() => require('../commands/meet').meetCommand(process.argv.slice(3)))
|
|
2239
2249
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
package/commands/gm.js
CHANGED
|
@@ -41,7 +41,9 @@ function showHelp() {
|
|
|
41
41
|
console.log(' what is up next. Every job a member takes in the last 30 days ends');
|
|
42
42
|
console.log(' landed (accepted or merged), dropped (claim let go), sent back, or');
|
|
43
43
|
console.log(' abandoned (archived without landing). Jobs in review are pending.');
|
|
44
|
-
console.log(' ovr =
|
|
44
|
+
console.log(' ovr = the Atris Rating of landed over finished, 25 to 99: 99 means 99');
|
|
45
|
+
console.log(' of every 100 and each halving of misses is worth about 6 (atris rating).');
|
|
46
|
+
console.log(' Rookie under 3 finished.');
|
|
45
47
|
console.log(' Drops and abandons count, so this is stricter than the website rating.');
|
|
46
48
|
console.log(' "on a job" means the claimed task moved in the last 6 hours; a claim');
|
|
47
49
|
console.log(' that sat longer shows as stuck, with its age and task id.');
|
|
@@ -936,8 +938,8 @@ function renderBoardLines(board, { interactive = false, width = termWidth(), bor
|
|
|
936
938
|
}
|
|
937
939
|
lines.push('');
|
|
938
940
|
|
|
939
|
-
lines.push(`key: ovr
|
|
940
|
-
lines.push('
|
|
941
|
+
lines.push(`key: ovr = the Atris Rating of finished jobs landed, 25 to 99, ${boxed ? 'new' : 'rookie'} under 3 finished.`);
|
|
942
|
+
lines.push(' 99 means 99 of every 100. drops and abandons count (atris rating)');
|
|
941
943
|
if (boxed) lines.push(' ·· = nothing to show yet: no finished job, no role, or no pinned model');
|
|
942
944
|
else lines.push(' form: last 5, newest right: k landed, d dropped, x sent back, a abandoned');
|
|
943
945
|
lines.push(' hours: claim to result. on a job: moved in the last 6h. review: pending');
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris rating: the Atris Rating, one number from 25 to 99 for how much real
|
|
4
|
+
// work a teammate finishes. Engines are rated in the combine (Loop Bench);
|
|
5
|
+
// members are rated on the jobs they land here, the same numbers atris gm
|
|
6
|
+
// shows. Read only: nothing runs and nothing is written.
|
|
7
|
+
|
|
8
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
9
|
+
const { modelLabel } = require('../lib/roster-models');
|
|
10
|
+
const rating = require('../lib/atris-rating');
|
|
11
|
+
|
|
12
|
+
const USAGE = [
|
|
13
|
+
'usage: atris rating [engine | member] [--json]',
|
|
14
|
+
' how much real work each AI teammate finishes, one number from 25 to 99.',
|
|
15
|
+
'',
|
|
16
|
+
' atris rating every engine the combine rated, and your members here',
|
|
17
|
+
' atris rating opus one engine\'s card (opus, sonnet, haiku, or a model id)',
|
|
18
|
+
' atris rating <member> one member\'s rating here, next to its engine\'s',
|
|
19
|
+
' --json print it as json',
|
|
20
|
+
].join('\n');
|
|
21
|
+
|
|
22
|
+
// "an 86", "a 76": eighty-something, eleven, and eighteen start with a vowel.
|
|
23
|
+
function rated(n) {
|
|
24
|
+
return `${/^(8|11$|18$)/.test(String(n)) ? 'an' : 'a'} ${n}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Members rated on the last 30 days of jobs here, best first, each with the
|
|
28
|
+
// job and model the roster gives it, plus the build line: what the build
|
|
29
|
+
// engine rates in the combine and what members doing build landed here.
|
|
30
|
+
// null when there is no task list or no finished job to rate.
|
|
31
|
+
function readHere(deps = {}) {
|
|
32
|
+
let root = deps.root;
|
|
33
|
+
let played = deps.season;
|
|
34
|
+
try {
|
|
35
|
+
if (!root) root = require('../lib/task-db').workspaceRoot(process.cwd());
|
|
36
|
+
if (!played) played = require('../lib/gm-season').loadSeason(root, { lastSeen: null });
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!played || played.error || played.no_history) return null;
|
|
41
|
+
const { rosterRows } = require('../lib/gm-season');
|
|
42
|
+
const rows = rosterRows({ outcomes: played.outcomes || [], limit: Number.MAX_SAFE_INTEGER }).filter((row) => row.finished > 0);
|
|
43
|
+
if (!rows.length) return null;
|
|
44
|
+
let lineup = deps.lineup;
|
|
45
|
+
if (lineup === undefined) lineup = require('../lib/team-lineup').readLineupSafe(root);
|
|
46
|
+
const team = new Map(((lineup && lineup.ok && lineup.team) || []).map((row) => [String(row.member || '').toLowerCase(), row]));
|
|
47
|
+
const members = rows.map((row) => {
|
|
48
|
+
const seat = team.get(row.name.toLowerCase()) || {};
|
|
49
|
+
return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, job: seat.job || null, model: seat.model || null };
|
|
50
|
+
});
|
|
51
|
+
const buildJob = ((lineup && lineup.ok && lineup.jobs) || []).find((row) => row.job === 'build' && !row.key);
|
|
52
|
+
const buildModel = buildJob && buildJob.now_runs ? buildJob.now_runs.model : null;
|
|
53
|
+
const card = buildModel ? rating.combineCards().find((c) => c.model === buildModel) : null;
|
|
54
|
+
const builders = members.filter((m) => m.job === 'build');
|
|
55
|
+
const landed = builders.reduce((sum, m) => sum + m.landed, 0);
|
|
56
|
+
const finished = builders.reduce((sum, m) => sum + m.finished, 0);
|
|
57
|
+
return {
|
|
58
|
+
workspace: played.workspace_name || null,
|
|
59
|
+
members,
|
|
60
|
+
build: card && finished ? { model: buildModel, combine_ovr: card.ovr, landed, finished, ovr: rating.atrisRating(landed, finished) } : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderHere(here) {
|
|
65
|
+
const ranked = here.members.filter((m) => m.ovr != null);
|
|
66
|
+
const fresh = here.members.filter((m) => m.ovr == null);
|
|
67
|
+
const nameW = Math.min(24, Math.max(6, ...ranked.map((m) => m.member.length))) + 2;
|
|
68
|
+
const lines = [`here${here.workspace ? `: ${here.workspace}` : ''}, last 30 days, the same ratings as atris gm`, ''];
|
|
69
|
+
if (ranked.length) {
|
|
70
|
+
lines.push(` ${'ovr'.padEnd(5)}${'member'.padEnd(nameW)}${'landed'.padEnd(11)}${'job'.padEnd(8)}model`);
|
|
71
|
+
for (const m of ranked) {
|
|
72
|
+
const name = m.member.length > nameW - 2 ? `${m.member.slice(0, nameW - 5)}...` : m.member;
|
|
73
|
+
lines.push(` ${String(m.ovr).padEnd(5)}${name.padEnd(nameW)}${`${m.landed} of ${m.finished}`.padEnd(11)}${(m.job || '').padEnd(8)}${m.model ? modelLabel(m.model) : ''}`.trimEnd());
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(' no member has 3 finished jobs here yet');
|
|
77
|
+
}
|
|
78
|
+
if (fresh.length) {
|
|
79
|
+
const names = fresh.map((m) => m.member);
|
|
80
|
+
const head = `new, under 3 finished jobs (${names.length}): `;
|
|
81
|
+
const shown = [];
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
if (head.length + [...shown, name].join(', ').length + ', and 99 more'.length > 80) break;
|
|
84
|
+
shown.push(name);
|
|
85
|
+
}
|
|
86
|
+
const rest = names.length - shown.length;
|
|
87
|
+
lines.push('', `${head}${shown.join(', ')}${rest ? `, and ${rest} more` : ''}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push('landed: accepted or merged. drops and abandons count against a member.');
|
|
90
|
+
if (here.build) {
|
|
91
|
+
const b = here.build;
|
|
92
|
+
lines.push('', `build runs ${modelLabel(b.model)}, ${rated(b.combine_ovr)} in the combine.`,
|
|
93
|
+
`members doing build here landed ${b.landed} of ${b.finished} jobs, ${rated(b.ovr)}.`);
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderMember(m, card) {
|
|
99
|
+
const job = m.job ? `, ${m.job}${m.model ? ` on ${modelLabel(m.model)}` : ''}` : '';
|
|
100
|
+
const head = `${m.member}${job}`;
|
|
101
|
+
const lines = [
|
|
102
|
+
`${head}${' '.repeat(Math.max(2, 44 - head.length))}${m.ovr == null ? 'new' : `${m.ovr} ovr`} here`,
|
|
103
|
+
'',
|
|
104
|
+
` landed ${m.landed} of ${m.finished} finished jobs in the last 30 days`,
|
|
105
|
+
];
|
|
106
|
+
if (m.ovr == null) lines.push(' rated once it has 3 finished jobs');
|
|
107
|
+
if (card && m.job === 'build') lines.push(` its engine, ${card.label}, is ${rated(card.ovr)} in the combine: atris rating ${card.label.split(' ')[0]}`);
|
|
108
|
+
lines.push(` every play: atris gm card ${m.member}`);
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ratingCommand(args = [], deps = {}) {
|
|
113
|
+
const write = deps.write || ((text) => process.stdout.write(text));
|
|
114
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
|
|
115
|
+
write(`${USAGE}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const json = hasFlag(args, '--json');
|
|
119
|
+
const name = args.filter((arg) => !arg.startsWith('-')).join(' ').trim();
|
|
120
|
+
const cards = rating.combineCards();
|
|
121
|
+
if (name) {
|
|
122
|
+
const card = rating.combineCardFor(name);
|
|
123
|
+
if (card) {
|
|
124
|
+
write(json ? `${JSON.stringify({ schema: 'atris.rating.v1', engine: card }, null, 2)}\n` : `${rating.renderCard(card)}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
const here = readHere(deps);
|
|
128
|
+
const member = here && here.members.find((m) => m.member.toLowerCase() === name.toLowerCase());
|
|
129
|
+
if (member) {
|
|
130
|
+
const own = member.model ? cards.find((c) => c.model === member.model) || null : null;
|
|
131
|
+
write(json ? `${JSON.stringify({ schema: 'atris.rating.v1', member, engine: member.job === 'build' ? own : null }, null, 2)}\n` : `${renderMember(member, own)}\n`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const err = deps.error || ((text) => process.stderr.write(text));
|
|
135
|
+
err(`no engine or member called "${name}" is rated. engines: ${cards.map((c) => c.label).join(', ')}. members show once they finish a job here.\n`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
const here = readHere(deps);
|
|
139
|
+
if (json) {
|
|
140
|
+
write(`${JSON.stringify({
|
|
141
|
+
schema: 'atris.rating.v1',
|
|
142
|
+
scale: { floor: 25, top: 99, means: rating.SCALE_LINES.join(' ') },
|
|
143
|
+
combine: { bench: rating.COMBINE.bench, month: rating.COMBINE.month, tasks: rating.COMBINE.tasks, unseen: rating.COMBINE.unseen, engines: cards },
|
|
144
|
+
here,
|
|
145
|
+
}, null, 2)}\n`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
write(`${rating.renderCombine(cards)}${here ? `\n\n${renderHere(here)}` : ''}\n`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { ratingCommand, USAGE };
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris scorecard: which AI can do your team's work, measured on your own
|
|
4
|
+
// past changes and graded by the tests that shipped with them. Finding tasks
|
|
5
|
+
// runs no AI. Tries run on your Claude plan login, never an API key.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { hasFlag, readFlag, readIntFlag, readNumberFlag } = require('../lib/arg-parser');
|
|
10
|
+
const sc = require('../lib/repo-scorecard');
|
|
11
|
+
|
|
12
|
+
const USAGE = [
|
|
13
|
+
'usage: atris scorecard [find | run | show] [options]',
|
|
14
|
+
' which AI can do your team\'s work, measured on your own past changes.',
|
|
15
|
+
'',
|
|
16
|
+
' atris scorecard find tasks the first time, then show the scorecard',
|
|
17
|
+
' atris scorecard find turn past changes into tasks their own tests grade (no AI runs)',
|
|
18
|
+
' atris scorecard run try each task with each model on your Claude plan',
|
|
19
|
+
' atris scorecard show print the scorecard from saved results',
|
|
20
|
+
'',
|
|
21
|
+
'find options:',
|
|
22
|
+
' --max N keep at most N tasks (default 12)',
|
|
23
|
+
' --scan N look through the last N changes (default 300)',
|
|
24
|
+
' --config FILE test commands for this repo (default .atris/scorecard.json)',
|
|
25
|
+
' --with-env copy .env files into each copy, read-only, billing keys left out',
|
|
26
|
+
'',
|
|
27
|
+
'run options:',
|
|
28
|
+
' --models a,b models to try (default haiku). any model past haiku needs',
|
|
29
|
+
' ATRIS_CLAUDE_SPEND_OK=<model> because it uses much more of your plan',
|
|
30
|
+
' --tries N tries per task and model (default 1). a stopped run picks up where it left off',
|
|
31
|
+
' --jobs N tries at a time (default 2)',
|
|
32
|
+
' --minutes N time limit per try (default 10, 20 for opus and fable)',
|
|
33
|
+
' --task ID only this task (its id or the first characters of its change)',
|
|
34
|
+
' --allow-extra-usage run even when extra usage is on for your Claude account',
|
|
35
|
+
'',
|
|
36
|
+
' --json print the result as json',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
function storePaths(root) {
|
|
40
|
+
const dir = sc.storeDir(root);
|
|
41
|
+
return { dir, tasks: path.join(dir, 'tasks.json'), results: path.join(dir, 'results.jsonl') };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readTasks(file) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readResults(file) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
55
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
56
|
+
}).filter(Boolean);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function firstLine(text, width) {
|
|
63
|
+
const line = String(text || '').split('\n')[0];
|
|
64
|
+
return line.length > width ? `${line.slice(0, width - 3)}...` : line;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SKIP_WORDS = {
|
|
68
|
+
no_tests: 'had no tests changed beside the code',
|
|
69
|
+
too_big: 'were too big for one ask',
|
|
70
|
+
short_ask: 'had too few words to work from',
|
|
71
|
+
unknown_runner: 'used a test setup this cannot run yet',
|
|
72
|
+
fails_after: 'had tests that fail even with the change',
|
|
73
|
+
passes_before: 'had tests that already passed before the change',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function skipSummary(skipped) {
|
|
77
|
+
return Object.entries(skipped || {}).filter(([, n]) => n > 0)
|
|
78
|
+
.map(([key, n]) => `${n} ${SKIP_WORDS[key] || key}`).join(', ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function find(root, args, { quiet = false } = {}) {
|
|
82
|
+
const paths = storePaths(root);
|
|
83
|
+
const json = hasFlag(args, '--json');
|
|
84
|
+
const say = (line) => { if (!json && !quiet) console.log(line); };
|
|
85
|
+
const max = readIntFlag(args, '--max', sc.MAX_TASKS);
|
|
86
|
+
const scan = readIntFlag(args, '--scan', 300);
|
|
87
|
+
say(`atris scorecard ${sc.repoName(root)}`);
|
|
88
|
+
say(`looking through the last ${scan} changes for ones whose own tests can grade an AI. no AI runs in this step.`);
|
|
89
|
+
say('');
|
|
90
|
+
const scratch = sc.workDir(root);
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await sc.findTasks(root, {
|
|
94
|
+
max,
|
|
95
|
+
scan,
|
|
96
|
+
configPath: readFlag(args, '--config', '') || undefined,
|
|
97
|
+
withEnv: hasFlag(args, '--with-env'),
|
|
98
|
+
workDir: scratch,
|
|
99
|
+
onCheck: (task, verdict) => {
|
|
100
|
+
const word = verdict === 'found' ? 'task' : 'skip';
|
|
101
|
+
const why = verdict === 'found' ? sc.runnerLabel(task.runner) : SKIP_WORDS[verdict];
|
|
102
|
+
say(` ${word} ${task.fix.slice(0, 7)} ${firstLine(task.ask, 60).padEnd(60)} ${why}`);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
} finally {
|
|
106
|
+
sc.stopAll();
|
|
107
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
const saved = {
|
|
110
|
+
repo: sc.repoName(root),
|
|
111
|
+
root,
|
|
112
|
+
found_at: new Date().toISOString(),
|
|
113
|
+
looked: result.looked,
|
|
114
|
+
candidates: result.candidates,
|
|
115
|
+
skipped: result.skipped,
|
|
116
|
+
tasks: result.tasks,
|
|
117
|
+
};
|
|
118
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
119
|
+
fs.writeFileSync(paths.tasks, `${JSON.stringify(saved, null, 2)}\n`);
|
|
120
|
+
if (json) {
|
|
121
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (!quiet) {
|
|
125
|
+
say('');
|
|
126
|
+
say(`found ${result.tasks.length} ${result.tasks.length === 1 ? 'task' : 'tasks'} in ${result.looked} past changes.`);
|
|
127
|
+
const skips = skipSummary(result.skipped);
|
|
128
|
+
if (skips) say(`the rest ${skips}.`);
|
|
129
|
+
if (!result.tasks.length) {
|
|
130
|
+
say('');
|
|
131
|
+
say('if your tests need a command or settings to run, put them in .atris/scorecard.json, for example:');
|
|
132
|
+
say(' {"tests": [{"match": "^backend/", "dir": "backend", "cmd": "python -m pytest -q {files}", "env": {"ENV": "test"}}]}');
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
say('');
|
|
136
|
+
say('next: atris scorecard run');
|
|
137
|
+
say(` tries each task with ${sc.shortModel(sc.DEFAULT_MODELS[0])} on your Claude plan, up to 10 minutes each, 2 at a time.`);
|
|
138
|
+
}
|
|
139
|
+
return result.tasks.length ? 0 : 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function pickTasks(saved, args) {
|
|
143
|
+
const only = readFlag(args, '--task', '');
|
|
144
|
+
if (!only) return saved.tasks;
|
|
145
|
+
return saved.tasks.filter((task) => task.id === only || task.id.startsWith(only) || task.fix.startsWith(only));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function run(root, args) {
|
|
149
|
+
const paths = storePaths(root);
|
|
150
|
+
const json = hasFlag(args, '--json');
|
|
151
|
+
const say = (line) => { if (!json) console.log(line); };
|
|
152
|
+
let saved = readTasks(paths.tasks);
|
|
153
|
+
if (!saved) {
|
|
154
|
+
say('no tasks yet, so finding them first.');
|
|
155
|
+
say('');
|
|
156
|
+
const code = await find(root, args.filter((a) => a !== '--json'), { quiet: json });
|
|
157
|
+
if (code !== 0) return code;
|
|
158
|
+
saved = readTasks(paths.tasks);
|
|
159
|
+
say('');
|
|
160
|
+
}
|
|
161
|
+
const tasks = pickTasks(saved, args);
|
|
162
|
+
if (!tasks.length) {
|
|
163
|
+
console.error(saved.tasks.length ? 'no task matches --task.' : 'there are no tasks to run. try atris scorecard find --scan 1000');
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
const models = sc.normalizeModels(readFlag(args, '--models', ''));
|
|
167
|
+
let login;
|
|
168
|
+
try {
|
|
169
|
+
login = sc.preflight(models, { allowExtraUsage: hasFlag(args, '--allow-extra-usage') });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(err.message || String(err));
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const tries = Math.max(1, readIntFlag(args, '--tries', 1) || 1);
|
|
175
|
+
const jobs = Math.max(1, readIntFlag(args, '--jobs', 2) || 2);
|
|
176
|
+
const minutes = readNumberFlag(args, '--minutes', null);
|
|
177
|
+
const before = readResults(paths.results);
|
|
178
|
+
const names = models.map(sc.shortModel).join(', ');
|
|
179
|
+
say(`atris scorecard run ${sc.repoName(root)}`);
|
|
180
|
+
say(`${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'} x ${names}, ${tries} ${tries === 1 ? 'try' : 'tries'} each, ${jobs} at a time.`);
|
|
181
|
+
say(`on your ${login.plan}. pay-per-use keys are removed from every run${login.extraUsage ? '' : ', so this bills $0'}.`);
|
|
182
|
+
say('');
|
|
183
|
+
|
|
184
|
+
const scratch = sc.workDir(root);
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
sc.stopAll();
|
|
187
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
188
|
+
};
|
|
189
|
+
const onSignal = () => {
|
|
190
|
+
cleanup();
|
|
191
|
+
console.error('\nstopped. finished tries are saved; run the same command to pick up where it left off.');
|
|
192
|
+
process.exit(130);
|
|
193
|
+
};
|
|
194
|
+
process.once('SIGINT', onSignal);
|
|
195
|
+
process.once('SIGTERM', onSignal);
|
|
196
|
+
let outcome;
|
|
197
|
+
try {
|
|
198
|
+
outcome = await sc.runAll(root, tasks, {
|
|
199
|
+
models,
|
|
200
|
+
tries,
|
|
201
|
+
jobs,
|
|
202
|
+
minutes,
|
|
203
|
+
bin: login.bin,
|
|
204
|
+
withEnv: hasFlag(args, '--with-env'),
|
|
205
|
+
done: before,
|
|
206
|
+
workDir: scratch,
|
|
207
|
+
outDir: paths.dir,
|
|
208
|
+
onResult: (row) => {
|
|
209
|
+
fs.appendFileSync(paths.results, `${JSON.stringify(row)}\n`);
|
|
210
|
+
const task = tasks.find((t) => t.id === row.id) || { fix: row.id, ask: '' };
|
|
211
|
+
let word = row.passed ? 'pass' : 'fail';
|
|
212
|
+
let note = row.timed_out ? `${row.limit_minutes} min (time limit)` : `${row.minutes} min`;
|
|
213
|
+
if (row.engine_error) { word = 'skip'; note = firstLine(row.engine_tail, 50) || 'claude did not finish'; }
|
|
214
|
+
if (row.harness_error) { word = 'skip'; note = firstLine(row.harness_error, 50); }
|
|
215
|
+
say(` ${word} ${sc.shortModel(row.model).padEnd(10)} ${task.fix.slice(0, 7)} ${firstLine(task.ask, 50).padEnd(50)} ${note}`);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
} finally {
|
|
219
|
+
process.removeListener('SIGINT', onSignal);
|
|
220
|
+
process.removeListener('SIGTERM', onSignal);
|
|
221
|
+
cleanup();
|
|
222
|
+
}
|
|
223
|
+
const rows = readResults(paths.results);
|
|
224
|
+
const summary = sc.summarize(saved.tasks, rows, models);
|
|
225
|
+
if (json) {
|
|
226
|
+
console.log(JSON.stringify({ ...summary, stopped: outcome.stopped, ran: outcome.rows.length }, null, 2));
|
|
227
|
+
return outcome.stopped ? 1 : 0;
|
|
228
|
+
}
|
|
229
|
+
if (!outcome.planned) say('every try asked for is already done.');
|
|
230
|
+
say('');
|
|
231
|
+
console.log(sc.renderScorecard(summary, { repo: sc.repoName(root), plan: login.plan, extraUsage: login.extraUsage }));
|
|
232
|
+
if (outcome.stopped) {
|
|
233
|
+
const billed = outcome.rows.find((row) => row.not_plan);
|
|
234
|
+
say('');
|
|
235
|
+
say(billed
|
|
236
|
+
? `stopped: ${billed.engine_tail} log in to your Claude plan with \`claude\` then /login, and run it again.`
|
|
237
|
+
: 'stopped early: your Claude plan hit its usage limit. run the same command after it resets to finish.');
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function show(root, args) {
|
|
244
|
+
const paths = storePaths(root);
|
|
245
|
+
const saved = readTasks(paths.tasks);
|
|
246
|
+
if (!saved) {
|
|
247
|
+
console.error('no scorecard yet. run: atris scorecard find');
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
const rows = readResults(paths.results);
|
|
251
|
+
const summary = sc.summarize(saved.tasks, rows);
|
|
252
|
+
if (hasFlag(args, '--json')) {
|
|
253
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (!rows.some((row) => saved.tasks.some((task) => task.id === row.id))) {
|
|
257
|
+
console.log(`atris scorecard ${saved.repo}`);
|
|
258
|
+
console.log(`${saved.tasks.length} tasks found, none tried yet:`);
|
|
259
|
+
for (const task of saved.tasks) console.log(` ${task.fix.slice(0, 7)} ${firstLine(task.ask, 80)}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log('next: atris scorecard run');
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
const login = sc.planLogin();
|
|
265
|
+
console.log(sc.renderScorecard(summary, { repo: saved.repo, plan: login.ok ? login.plan : '', extraUsage: login.extraUsage }));
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function scorecardCommand(args = []) {
|
|
270
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
|
|
271
|
+
console.log(USAGE);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
let root;
|
|
275
|
+
try {
|
|
276
|
+
root = sc.repoRoot();
|
|
277
|
+
} catch {
|
|
278
|
+
console.error('atris scorecard works inside a git repo. cd into one and try again.');
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
const sub = args[0] && !args[0].startsWith('-') ? args[0] : '';
|
|
282
|
+
const rest = sub ? args.slice(1) : args;
|
|
283
|
+
if (sub === 'find') return find(root, rest);
|
|
284
|
+
if (sub === 'run') return run(root, rest);
|
|
285
|
+
if (sub === 'show') return show(root, rest);
|
|
286
|
+
if (sub) {
|
|
287
|
+
console.error(`unknown step "${sub}".\n\n${USAGE}`);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
if (!readTasks(storePaths(root).tasks)) {
|
|
291
|
+
const code = await find(root, rest);
|
|
292
|
+
return code;
|
|
293
|
+
}
|
|
294
|
+
return show(root, rest);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { scorecardCommand, USAGE };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bench": "Loop Bench",
|
|
3
|
+
"rated": "2026-10-29",
|
|
4
|
+
"tasks": 80,
|
|
5
|
+
"unseen": 32,
|
|
6
|
+
"engines": [
|
|
7
|
+
{
|
|
8
|
+
"model": "claude-opus-5-5",
|
|
9
|
+
"tasks": 80,
|
|
10
|
+
"finished": 77.0,
|
|
11
|
+
"python": {
|
|
12
|
+
"finished": 35.0,
|
|
13
|
+
"tasks": 36
|
|
14
|
+
},
|
|
15
|
+
"javascript": {
|
|
16
|
+
"finished": 42.0,
|
|
17
|
+
"tasks": 44
|
|
18
|
+
},
|
|
19
|
+
"unseen": {
|
|
20
|
+
"finished": 31.0,
|
|
21
|
+
"tasks": 32
|
|
22
|
+
},
|
|
23
|
+
"tries": 83,
|
|
24
|
+
"broke": 1,
|
|
25
|
+
"timed_out": 1,
|
|
26
|
+
"minutes_cap": 20,
|
|
27
|
+
"median_minutes": 2.23,
|
|
28
|
+
"cost_per_finish": 0.914
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"model": "claude-sonnet-5-5",
|
|
32
|
+
"tasks": 79,
|
|
33
|
+
"finished": 73.0,
|
|
34
|
+
"python": {
|
|
35
|
+
"finished": 33.6,
|
|
36
|
+
"tasks": 35
|
|
37
|
+
},
|
|
38
|
+
"javascript": {
|
|
39
|
+
"finished": 39.4,
|
|
40
|
+
"tasks": 44
|
|
41
|
+
},
|
|
42
|
+
"unseen": {
|
|
43
|
+
"finished": 27.0,
|
|
44
|
+
"tasks": 32
|
|
45
|
+
},
|
|
46
|
+
"tries": 140,
|
|
47
|
+
"broke": 3,
|
|
48
|
+
"timed_out": 5,
|
|
49
|
+
"minutes_cap": 10,
|
|
50
|
+
"median_minutes": 1.46,
|
|
51
|
+
"cost_per_finish": 0.428
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"model": "claude-haiku-5-5",
|
|
55
|
+
"tasks": 80,
|
|
56
|
+
"finished": 70.5,
|
|
57
|
+
"python": {
|
|
58
|
+
"finished": 30.8,
|
|
59
|
+
"tasks": 36
|
|
60
|
+
},
|
|
61
|
+
"javascript": {
|
|
62
|
+
"finished": 39.8,
|
|
63
|
+
"tasks": 44
|
|
64
|
+
},
|
|
65
|
+
"unseen": {
|
|
66
|
+
"finished": 27.2,
|
|
67
|
+
"tasks": 32
|
|
68
|
+
},
|
|
69
|
+
"tries": 545,
|
|
70
|
+
"broke": 8,
|
|
71
|
+
"timed_out": 87,
|
|
72
|
+
"minutes_cap": 10,
|
|
73
|
+
"median_minutes": 2.48,
|
|
74
|
+
"cost_per_finish": 0.082
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|