atris 3.74.24 → 3.74.26

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 CHANGED
@@ -459,6 +459,22 @@ atris scorecard run --models haiku,opus # compare models; past Haiku needs AT
459
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
460
  - Tests that need their own command or settings go in `.atris/scorecard.json`.
461
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
+
462
478
  ## Benchmark Harness
463
479
 
464
480
  Atris ships one public head-to-head benchmark harness for comparing a pinned
package/bin/atris.js CHANGED
@@ -652,6 +652,7 @@ function showHelpAll() {
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
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)');
655
656
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
656
657
  console.log(' lesson - atris lesson add <slug> <pass|fail> "<text>" (mine/sweep/resolve/revert/ledger; --detector validates the falsifier at write time)');
657
658
  console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
@@ -2239,6 +2240,10 @@ if (command === 'guide') {
2239
2240
  Promise.resolve().then(() => require('../commands/scorecard').scorecardCommand(process.argv.slice(3)))
2240
2241
  .then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
2241
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); });
2242
2247
  } else if (command === 'meet') {
2243
2248
  Promise.resolve().then(() => require('../commands/meet').meetCommand(process.argv.slice(3)))
2244
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 = round(60 + 39 * landed / finished), rookie under 3 finished.');
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 60 to 99 = share of finished jobs landed, ${boxed ? 'new' : 'rookie'} under 3 finished,`);
940
- lines.push(' stricter than the website rating on purpose (drops and abandons count)');
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');
@@ -1893,6 +1895,8 @@ function wakeableMember(args) {
1893
1895
  }
1894
1896
 
1895
1897
  async function gmCommand(...args) {
1898
+ const leaseDb = require('../lib/task-db');
1899
+ try { leaseDb.sweepExpiredLeases(); } catch (_) { /* Existing views report unreadable databases. */ }
1896
1900
  // the walk has its own help
1897
1901
  if (positional(args)[0] === 'walk') {
1898
1902
  const at = args.indexOf('walk');
@@ -0,0 +1,166 @@
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
+ const reasons = new Map();
50
+ for (const play of played.outcomes || []) {
51
+ if (play.member === row.name && ['dropped', 'sent_back', 'abandoned'].includes(play.kind) && play.reason) {
52
+ reasons.set(play.reason, (reasons.get(play.reason) || 0) + 1);
53
+ }
54
+ }
55
+ const miss_reasons = [...reasons].map(([reason, count]) => ({ reason, count }))
56
+ .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)).slice(0, 3);
57
+ return { member: row.name, ovr: row.ovr, landed: row.landed, finished: row.finished, job: seat.job || null, model: seat.model || null, ...(miss_reasons.length ? { miss_reasons } : {}) };
58
+ });
59
+ const buildJob = ((lineup && lineup.ok && lineup.jobs) || []).find((row) => row.job === 'build' && !row.key);
60
+ const buildModel = buildJob && buildJob.now_runs ? buildJob.now_runs.model : null;
61
+ const card = buildModel ? rating.combineCards().find((c) => c.model === buildModel) : null;
62
+ const builders = members.filter((m) => m.job === 'build');
63
+ const landed = builders.reduce((sum, m) => sum + m.landed, 0);
64
+ const finished = builders.reduce((sum, m) => sum + m.finished, 0);
65
+ return {
66
+ workspace: played.workspace_name || null,
67
+ members,
68
+ build: card && finished ? { model: buildModel, combine_ovr: card.ovr, landed, finished, ovr: rating.atrisRating(landed, finished) } : null,
69
+ };
70
+ }
71
+
72
+ function renderHere(here) {
73
+ const ranked = here.members.filter((m) => m.ovr != null);
74
+ const fresh = here.members.filter((m) => m.ovr == null);
75
+ const nameW = Math.min(24, Math.max(6, ...ranked.map((m) => m.member.length))) + 2;
76
+ const lines = [`here${here.workspace ? `: ${here.workspace}` : ''}, last 30 days, the same ratings as atris gm`, ''];
77
+ if (ranked.length) {
78
+ lines.push(` ${'ovr'.padEnd(5)}${'member'.padEnd(nameW)}${'landed'.padEnd(11)}${'job'.padEnd(8)}model`);
79
+ for (const m of ranked) {
80
+ const name = m.member.length > nameW - 2 ? `${m.member.slice(0, nameW - 5)}...` : m.member;
81
+ 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());
82
+ }
83
+ } else {
84
+ lines.push(' no member has 3 finished jobs here yet');
85
+ }
86
+ if (fresh.length) {
87
+ const names = fresh.map((m) => m.member);
88
+ const head = `new, under 3 finished jobs (${names.length}): `;
89
+ const shown = [];
90
+ for (const name of names) {
91
+ if (head.length + [...shown, name].join(', ').length + ', and 99 more'.length > 80) break;
92
+ shown.push(name);
93
+ }
94
+ const rest = names.length - shown.length;
95
+ lines.push('', `${head}${shown.join(', ')}${rest ? `, and ${rest} more` : ''}`);
96
+ }
97
+ lines.push('landed: accepted or merged. drops and abandons count against a member.');
98
+ if (here.build) {
99
+ const b = here.build;
100
+ lines.push('', `build runs ${modelLabel(b.model)}, ${rated(b.combine_ovr)} in the combine.`,
101
+ `members doing build here landed ${b.landed} of ${b.finished} jobs, ${rated(b.ovr)}.`);
102
+ }
103
+ return lines.join('\n');
104
+ }
105
+
106
+ function renderMember(m, card) {
107
+ const job = m.job ? `, ${m.job}${m.model ? ` on ${modelLabel(m.model)}` : ''}` : '';
108
+ const head = `${m.member}${job}`;
109
+ const lines = [
110
+ `${head}${' '.repeat(Math.max(2, 44 - head.length))}${m.ovr == null ? 'new' : `${m.ovr} ovr`} here`,
111
+ '',
112
+ ` landed ${m.landed} of ${m.finished} finished jobs in the last 30 days`,
113
+ ];
114
+ if (m.ovr == null) lines.push(' rated once it has 3 finished jobs');
115
+ if (m.miss_reasons && m.miss_reasons.length) {
116
+ lines.push(' top miss reasons:');
117
+ for (const item of m.miss_reasons) lines.push(` ${item.count}: ${item.reason}`);
118
+ }
119
+ if (card && m.job === 'build') lines.push(` its engine, ${card.label}, is ${rated(card.ovr)} in the combine: atris rating ${card.label.split(' ')[0]}`);
120
+ lines.push(` every play: atris gm card ${m.member}`);
121
+ return lines.join('\n');
122
+ }
123
+
124
+ function ratingCommand(args = [], deps = {}) {
125
+ const taskDb = require('../lib/task-db');
126
+ taskDb.sweepExpiredLeases();
127
+ const write = deps.write || ((text) => process.stdout.write(text));
128
+ if (hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help') {
129
+ write(`${USAGE}\n`);
130
+ return 0;
131
+ }
132
+ const json = hasFlag(args, '--json');
133
+ const name = args.filter((arg) => !arg.startsWith('-')).join(' ').trim();
134
+ const cards = rating.combineCards();
135
+ if (name) {
136
+ const card = rating.combineCardFor(name);
137
+ if (card) {
138
+ write(json ? `${JSON.stringify({ schema: 'atris.rating.v1', engine: card }, null, 2)}\n` : `${rating.renderCard(card)}\n`);
139
+ return 0;
140
+ }
141
+ const here = readHere(deps);
142
+ const member = here && here.members.find((m) => m.member.toLowerCase() === name.toLowerCase());
143
+ if (member) {
144
+ const own = member.model ? cards.find((c) => c.model === member.model) || null : null;
145
+ write(json ? `${JSON.stringify({ schema: 'atris.rating.v1', member, engine: member.job === 'build' ? own : null }, null, 2)}\n` : `${renderMember(member, own)}\n`);
146
+ return 0;
147
+ }
148
+ const err = deps.error || ((text) => process.stderr.write(text));
149
+ 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`);
150
+ return 1;
151
+ }
152
+ const here = readHere(deps);
153
+ if (json) {
154
+ write(`${JSON.stringify({
155
+ schema: 'atris.rating.v1',
156
+ scale: { floor: 25, top: 99, means: rating.SCALE_LINES.join(' ') },
157
+ combine: { bench: rating.COMBINE.bench, month: rating.COMBINE.month, tasks: rating.COMBINE.tasks, unseen: rating.COMBINE.unseen, engines: cards },
158
+ here,
159
+ }, null, 2)}\n`);
160
+ return 0;
161
+ }
162
+ write(`${rating.renderCombine(cards)}${here ? `\n\n${renderHere(here)}` : ''}\n`);
163
+ return 0;
164
+ }
165
+
166
+ module.exports = { ratingCommand, USAGE };
package/commands/task.js CHANGED
@@ -208,6 +208,7 @@ atris task - durable local task state (SQLite, gitignored)
208
208
  atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
209
209
  Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
210
210
  atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
211
+ atris task sweep [--json] [--stale-days N] [--claimer NAME] [--dry-run] Release expired claims
211
212
  atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
212
213
  atris task revise <id> --note "..." Send reviewed work back to Do
213
214
 
@@ -6252,7 +6253,10 @@ function cmdClaim(args) {
6252
6253
  const taskDb = getTaskDb();
6253
6254
  const db = taskDb.open();
6254
6255
  const taskId = requireTaskId(taskDb, db, id, 'atris task claim');
6255
- const result = taskDb.claimTask(db, { id: taskId, claimedBy: String(owner) });
6256
+ const leaseMinutes = flag(args, '--lease-minutes');
6257
+ const leasePid = flag(args, '--lease-pid');
6258
+ if (leaseMinutes != null && (!Number.isFinite(Number(leaseMinutes)) || Number(leaseMinutes) <= 0 || !Number.isInteger(Number(leasePid)) || Number(leasePid) <= 0)) throw new Error('invalid claim lease');
6259
+ const result = taskDb.claimTask(db, { id: taskId, claimedBy: String(owner), ...(leaseMinutes == null ? {} : { leaseMinutes: Number(leaseMinutes), leasePid: Number(leasePid) }) });
6256
6260
  if (result.claimed) {
6257
6261
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
6258
6262
  if (wantsJson(args)) {
@@ -10801,11 +10805,23 @@ function evaluateSweepAutoAccept(task, root, includeProtected = false) {
10801
10805
 
10802
10806
  function cmdSweep(args) {
10803
10807
  if (!hasFlag(args, '--auto-accept')) {
10804
- failTask(
10805
- 'atris task sweep',
10806
- 'missing_auto_accept',
10807
- 'atris task sweep currently requires --auto-accept for the explicit verified accept policy',
10808
- );
10808
+ const taskDb = getTaskDb();
10809
+ const staleRaw = flag(args, '--stale-days');
10810
+ if (staleRaw === true || (staleRaw != null && (!Number.isFinite(Number(staleRaw)) || Number(staleRaw) < 0))) throw new Error('stale days must be a nonnegative number');
10811
+ const claimer = flag(args, '--claimer');
10812
+ if (claimer === true) throw new Error('claimer requires a name');
10813
+ const dryRun = hasFlag(args, '--dry-run');
10814
+ const options = { staleDays: staleRaw == null ? undefined : Number(staleRaw), claimer };
10815
+ const tasks = dryRun ? taskDb.previewExpiredLeases(options) : taskDb.expireLeases(taskDb.open(), options);
10816
+ const groups = {};
10817
+ for (const task of tasks) (groups[task.claimed_by] ||= []).push(task);
10818
+ if (wantsJson(args)) printJson({ dry_run: dryRun, groups, tasks });
10819
+ else if (!tasks.length) console.log(dryRun ? 'no claims would be released' : 'no claims released');
10820
+ else for (const [name, entries] of Object.entries(groups)) {
10821
+ console.log(`${name}: ${entries.length} ${dryRun ? 'would be released' : 'released'}`);
10822
+ for (const entry of entries) console.log(` ${entry.title}: ${entry.reason}`);
10823
+ }
10824
+ return;
10809
10825
  }
10810
10826
  // Sweep has no dry-run implementation: every successful invocation can
10811
10827
  // write accepts, even when an unrecognized --dry-run flag is supplied.
@@ -13076,6 +13092,11 @@ function cmdServe(args) {
13076
13092
 
13077
13093
  async function runTaskCommand(args) {
13078
13094
  const raw = args || [];
13095
+ // A dry run must not mutate even already-expired leases.
13096
+ if (!raw.includes('--dry-run') && !(raw[0] === 'sweep' && !raw.includes('--auto-accept'))) {
13097
+ const taskDb = getTaskDb();
13098
+ taskDb.sweepExpiredLeases();
13099
+ }
13079
13100
  if (raw.includes('--help') || raw.includes('-h')) return help();
13080
13101
  const first = raw[0];
13081
13102
  if (!first) return cmdFirstMinute();
@@ -13273,7 +13294,7 @@ async function run(args) {
13273
13294
  return;
13274
13295
  }
13275
13296
  const result = await runTaskCommand(raw);
13276
- const skipsRender = sub === 'clear-done' && hasFlag(raw, '--dry-run');
13297
+ const skipsRender = hasFlag(raw, '--dry-run');
13277
13298
  if (MUTATING_TASK_COMMANDS.has(sub) && !skipsRender) autoRenderTodoFromDb();
13278
13299
  return result;
13279
13300
  }