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 +16 -0
- package/bin/atris.js +5 -0
- package/commands/gm.js +7 -3
- package/commands/rating.js +166 -0
- package/commands/task.js +28 -7
- package/commands/worktree.js +167 -76
- package/lib/atris-rating-combine.json +77 -0
- package/lib/atris-rating.js +179 -0
- package/lib/fleet.js +42 -2
- package/lib/gm-bet.js +2 -2
- package/lib/gm-season.js +67 -18
- package/lib/known-commands.js +1 -1
- package/lib/task-db.js +117 -7
- package/lib/team-lineup.js +6 -1
- package/package.json +1 -1
package/commands/worktree.js
CHANGED
|
@@ -1201,7 +1201,6 @@ const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
|
1201
1201
|
// (zero commits yet), so without a grace window the janitor reaps it while
|
|
1202
1202
|
// the engine that requested it is still booting inside.
|
|
1203
1203
|
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
1204
|
-
const COMPLETED_UNMERGED_REAP_MS = WORKTREE_REAP_GRACE_MS;
|
|
1205
1204
|
const GENERATED_WORKTREE_CACHE_DIRS = [
|
|
1206
1205
|
'.derivedData',
|
|
1207
1206
|
'.next',
|
|
@@ -1218,15 +1217,6 @@ function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
|
1218
1217
|
}
|
|
1219
1218
|
}
|
|
1220
1219
|
|
|
1221
|
-
function completedAgentOutputAgeMs(worktreePath, now = Date.now()) {
|
|
1222
|
-
try {
|
|
1223
|
-
const output = path.join(worktreePath, '.codex-last-message.txt');
|
|
1224
|
-
return now - fs.statSync(output).mtimeMs;
|
|
1225
|
-
} catch {
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
1220
|
function canonicalPath(value) {
|
|
1231
1221
|
try {
|
|
1232
1222
|
return fs.realpathSync(value);
|
|
@@ -1237,17 +1227,20 @@ function canonicalPath(value) {
|
|
|
1237
1227
|
|
|
1238
1228
|
// null means the scan failed: we cannot tell who is inside, so callers keep
|
|
1239
1229
|
// every folder. An empty list would read as "nobody is here" and delete.
|
|
1240
|
-
function
|
|
1241
|
-
const result = spawnSync('lsof', ['-n', '-
|
|
1230
|
+
function listActiveProcessFiles() {
|
|
1231
|
+
const result = spawnSync('lsof', ['-n', '-F', 'n'], {
|
|
1242
1232
|
encoding: 'utf8',
|
|
1243
1233
|
maxBuffer: COMMAND_MAX_BUFFER_BYTES,
|
|
1234
|
+
timeout: 20000,
|
|
1235
|
+
killSignal: 'SIGKILL',
|
|
1244
1236
|
});
|
|
1245
|
-
if (result.error || result.status !== 0) return null;
|
|
1246
|
-
|
|
1237
|
+
if (result.error || result.status !== 0 || String(result.stderr || '').trim()) return null;
|
|
1238
|
+
const paths = String(result.stdout || '')
|
|
1247
1239
|
.split(/\r?\n/)
|
|
1248
|
-
.filter((line) => line.startsWith('n'))
|
|
1240
|
+
.filter((line) => line.startsWith('n/'))
|
|
1249
1241
|
.map((line) => canonicalPath(line.slice(1)))
|
|
1250
1242
|
.filter(Boolean);
|
|
1243
|
+
return paths.length ? paths : null;
|
|
1251
1244
|
}
|
|
1252
1245
|
|
|
1253
1246
|
// Absolute paths named on each program's command line, including flag values
|
|
@@ -1274,17 +1267,20 @@ function listActiveProcessArgPaths() {
|
|
|
1274
1267
|
const result = spawnSync('ps', ['-axww', '-o', 'args='], {
|
|
1275
1268
|
encoding: 'utf8',
|
|
1276
1269
|
maxBuffer: COMMAND_MAX_BUFFER_BYTES,
|
|
1270
|
+
timeout: 5000,
|
|
1271
|
+
killSignal: 'SIGKILL',
|
|
1277
1272
|
});
|
|
1278
|
-
if (result.error || result.status !== 0) return
|
|
1273
|
+
if (result.error || result.status !== 0) return null;
|
|
1279
1274
|
return parseProcessArgPaths(result.stdout);
|
|
1280
1275
|
}
|
|
1281
1276
|
|
|
1282
|
-
//
|
|
1283
|
-
// script path on its command line.
|
|
1277
|
+
// Open files and working folders, plus script paths on command lines.
|
|
1284
1278
|
function listActiveProcessPaths() {
|
|
1285
|
-
const
|
|
1286
|
-
if (!Array.isArray(
|
|
1287
|
-
|
|
1279
|
+
const files = listActiveProcessFiles();
|
|
1280
|
+
if (!Array.isArray(files)) return null;
|
|
1281
|
+
const args = listActiveProcessArgPaths();
|
|
1282
|
+
if (!Array.isArray(args)) return null;
|
|
1283
|
+
return files.concat(args);
|
|
1288
1284
|
}
|
|
1289
1285
|
|
|
1290
1286
|
function worktreeHasActiveProcess(worktreePath, activeCwds) {
|
|
@@ -1311,7 +1307,44 @@ function recordWorktreeRemoval(root, { path: removedPath, branch = '', by, reaso
|
|
|
1311
1307
|
} catch {}
|
|
1312
1308
|
}
|
|
1313
1309
|
|
|
1314
|
-
|
|
1310
|
+
// Only webpack's disposable pack files have a sufficiently narrow contract
|
|
1311
|
+
// for unattended cleanup. Unknown files or links turn the folder into review.
|
|
1312
|
+
function webpackCacheSnapshot(worktreePath) {
|
|
1313
|
+
try {
|
|
1314
|
+
const root = fs.realpathSync(worktreePath);
|
|
1315
|
+
let current = root;
|
|
1316
|
+
for (const part of ['.next', 'cache', 'webpack']) {
|
|
1317
|
+
current = path.join(current, part);
|
|
1318
|
+
const stat = fs.lstatSync(current);
|
|
1319
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
1320
|
+
}
|
|
1321
|
+
if (fs.realpathSync(current) !== current) return null;
|
|
1322
|
+
const entries = [];
|
|
1323
|
+
function walk(dir) {
|
|
1324
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
1325
|
+
const file = path.join(dir, name);
|
|
1326
|
+
const stat = fs.lstatSync(file);
|
|
1327
|
+
if (stat.isSymbolicLink()) return false;
|
|
1328
|
+
if (stat.isDirectory()) {
|
|
1329
|
+
entries.push([file, 'd', stat.ino, stat.mtimeMs]);
|
|
1330
|
+
if (!walk(file)) return false;
|
|
1331
|
+
} else if (stat.isFile() && /^(?:index|\d+)\.pack(?:\.gz|\.old)?$/.test(name)) {
|
|
1332
|
+
entries.push([file, 'f', stat.ino, stat.size, stat.mtimeMs]);
|
|
1333
|
+
} else return false;
|
|
1334
|
+
}
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
if (!walk(current)) return null;
|
|
1338
|
+
const size = spawnSync('du', ['-sk', current], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL' });
|
|
1339
|
+
const match = size.status === 0 && String(size.stdout || '').match(/^(\d+)\s/);
|
|
1340
|
+
if (!match) return null;
|
|
1341
|
+
return { path: current, kib: Number(match[1]), fingerprint: JSON.stringify(entries) };
|
|
1342
|
+
} catch {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function pruneGeneratedWorktreeCaches(worktreePath, { apply = false, budget } = {}) {
|
|
1315
1348
|
const candidates = [];
|
|
1316
1349
|
const pruned = [];
|
|
1317
1350
|
for (const relative of GENERATED_WORKTREE_CACHE_DIRS) {
|
|
@@ -1319,34 +1352,70 @@ function pruneGeneratedWorktreeCaches(worktreePath, { apply = false } = {}) {
|
|
|
1319
1352
|
if (!fs.existsSync(target)) continue;
|
|
1320
1353
|
const ignored = runGit(['check-ignore', '-q', '--', relative], { cwd: worktreePath, check: false });
|
|
1321
1354
|
if (ignored.status !== 0) continue;
|
|
1322
|
-
const item = { worktree: worktreePath, path: target, relative };
|
|
1355
|
+
const item = { worktree: worktreePath, path: target, relative, reason: 'manual_review_required' };
|
|
1323
1356
|
candidates.push(item);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1357
|
+
}
|
|
1358
|
+
const relative = '.next/cache/webpack';
|
|
1359
|
+
const ignored = runGit(['check-ignore', '-q', '--', relative], { cwd: worktreePath, check: false });
|
|
1360
|
+
const snapshot = ignored.status === 0 ? webpackCacheSnapshot(worktreePath) : null;
|
|
1361
|
+
if (snapshot) {
|
|
1362
|
+
const item = { worktree: worktreePath, path: snapshot.path, relative, kib: snapshot.kib, reason: 'generated_webpack_cache' };
|
|
1363
|
+
candidates.push(item);
|
|
1364
|
+
if (apply && budget && budget.count < 20 && budget.kib + snapshot.kib <= 10 * 1024 * 1024) {
|
|
1365
|
+
const active = listActiveProcessPaths();
|
|
1366
|
+
const fresh = webpackCacheSnapshot(worktreePath);
|
|
1367
|
+
const stillIgnored = runGit(['check-ignore', '-q', '--', relative], { cwd: worktreePath, check: false });
|
|
1368
|
+
if (Array.isArray(active) && !worktreeHasActiveProcess(worktreePath, active)
|
|
1369
|
+
&& stillIgnored.status === 0 && fresh && fresh.path === snapshot.path
|
|
1370
|
+
&& fresh.kib === snapshot.kib && fresh.fingerprint === snapshot.fingerprint) {
|
|
1371
|
+
try {
|
|
1372
|
+
fs.rmSync(snapshot.path, { recursive: true });
|
|
1373
|
+
budget.count += 1;
|
|
1374
|
+
budget.kib += snapshot.kib;
|
|
1375
|
+
pruned.push(item);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
item.error = String((error && error.message) || error);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1330
1380
|
}
|
|
1331
1381
|
}
|
|
1332
1382
|
return { candidates, pruned };
|
|
1333
1383
|
}
|
|
1334
1384
|
|
|
1385
|
+
function originBaseIsCurrent(root, base) {
|
|
1386
|
+
const prefix = base.startsWith('origin/') ? 'origin/'
|
|
1387
|
+
: base.startsWith('refs/remotes/origin/') ? 'refs/remotes/origin/' : '';
|
|
1388
|
+
if (!prefix) {
|
|
1389
|
+
if (base === 'HEAD') {
|
|
1390
|
+
const branch = runGit(['symbolic-ref', '--short', 'HEAD'], { cwd: root, check: false });
|
|
1391
|
+
return branch.status === 0 && PROTECTED_BRANCHES.has(branch.stdout.trim());
|
|
1392
|
+
}
|
|
1393
|
+
return PROTECTED_BRANCHES.has(base.replace(/^refs\/heads\//, ''));
|
|
1394
|
+
}
|
|
1395
|
+
const branch = base.slice(prefix.length);
|
|
1396
|
+
const local = runGit(['rev-parse', '--verify', base], { cwd: root, check: false });
|
|
1397
|
+
const remote = runGit(['ls-remote', '--exit-code', 'origin', `refs/heads/${branch}`], { cwd: root, check: false, timeout: 5000 });
|
|
1398
|
+
const match = String(remote.stdout || '').match(/^([a-f0-9]{40,64})\s+(refs\/heads\/[^\r\n]+)$/m);
|
|
1399
|
+
return local.status === 0 && remote.status === 0 && Boolean(match)
|
|
1400
|
+
&& match[2] === `refs/heads/${branch}` && local.stdout.trim() === match[1];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1335
1403
|
function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply = false, activeCwds: activeCwdsOverride, by = 'atris worktree cleanup' } = {}) {
|
|
1336
1404
|
const worktrees = listWorktrees(root);
|
|
1337
1405
|
const primary = worktrees[0]?.path ? path.resolve(worktrees[0].path) : '';
|
|
1338
1406
|
const current = path.resolve(root);
|
|
1339
1407
|
const activeCwds = Array.isArray(activeCwdsOverride) ? activeCwdsOverride.map(canonicalPath) : listActiveProcessPaths();
|
|
1340
1408
|
const base = normalizeTargetRef(root, baseOverride || defaultShipTarget(root));
|
|
1341
|
-
refreshRemoteRef(root, base);
|
|
1409
|
+
const refreshError = refreshRemoteRef(root, base);
|
|
1342
1410
|
const candidates = [];
|
|
1343
1411
|
const kept = [];
|
|
1344
1412
|
const removed = [];
|
|
1345
1413
|
const cacheCandidates = [];
|
|
1346
1414
|
const cachePruned = [];
|
|
1415
|
+
const cacheBudget = { count: 0, kib: 0 };
|
|
1347
1416
|
const pruneCachesForKept = (wtPath) => {
|
|
1348
1417
|
if (worktreeWithinReapGrace(wtPath)) return;
|
|
1349
|
-
const caches = pruneGeneratedWorktreeCaches(wtPath, { apply });
|
|
1418
|
+
const caches = pruneGeneratedWorktreeCaches(wtPath, { apply, budget: cacheBudget });
|
|
1350
1419
|
cacheCandidates.push(...caches.candidates);
|
|
1351
1420
|
cachePruned.push(...caches.pruned);
|
|
1352
1421
|
};
|
|
@@ -1374,9 +1443,8 @@ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply =
|
|
|
1374
1443
|
kept.push({ ...item, reason: activeCwds ? 'active_process' : 'process_scan_failed' });
|
|
1375
1444
|
continue;
|
|
1376
1445
|
}
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
if (!rawCounts || !counts) {
|
|
1446
|
+
const counts = statusCounts(wt.path);
|
|
1447
|
+
if (!counts) {
|
|
1380
1448
|
kept.push({ ...item, reason: 'missing_or_unreadable' });
|
|
1381
1449
|
continue;
|
|
1382
1450
|
}
|
|
@@ -1385,58 +1453,78 @@ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply =
|
|
|
1385
1453
|
kept.push({ ...item, reason: 'dirty', staged: counts.staged, unstaged: counts.unstaged, untracked: counts.untracked });
|
|
1386
1454
|
continue;
|
|
1387
1455
|
}
|
|
1456
|
+
// Git removes ignored files along with a worktree. They may include local
|
|
1457
|
+
// state, credentials, or user data, so a clean status is not enough proof.
|
|
1458
|
+
const ignored = runGit(['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'], { cwd: wt.path, check: false });
|
|
1459
|
+
if (ignored.status !== 0 || ignored.error) {
|
|
1460
|
+
kept.push({ ...item, reason: 'ignored_scan_failed' });
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (ignored.stdout) {
|
|
1464
|
+
pruneCachesForKept(wtPath);
|
|
1465
|
+
kept.push({ ...item, reason: 'ignored_data' });
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1388
1468
|
if (!wt.head) {
|
|
1389
1469
|
pruneCachesForKept(wtPath);
|
|
1390
1470
|
kept.push({ ...item, reason: 'missing_head' });
|
|
1391
1471
|
continue;
|
|
1392
1472
|
}
|
|
1393
|
-
|
|
1394
|
-
(
|
|
1395
|
-
|
|
1396
|
-
|
|
1473
|
+
if (refreshError) {
|
|
1474
|
+
kept.push({ ...item, reason: 'base_refresh_failed' });
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1397
1477
|
const merged = runGit(['merge-base', '--is-ancestor', wt.head, base], { cwd: root, check: false }).status === 0;
|
|
1398
1478
|
if (!merged) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
pruneCachesForKept(wtPath);
|
|
1402
|
-
kept.push({
|
|
1403
|
-
...item,
|
|
1404
|
-
reason: completedAgeMs === null ? 'unmerged' : 'completed_unmerged_retention',
|
|
1405
|
-
...(completedAgeMs === null ? {} : { retention_hours: 1 }),
|
|
1406
|
-
});
|
|
1407
|
-
continue;
|
|
1408
|
-
}
|
|
1409
|
-
const candidate = {
|
|
1410
|
-
...item,
|
|
1411
|
-
reason: 'completed_unmerged_checkout_expired',
|
|
1412
|
-
branch_preserved: true,
|
|
1413
|
-
artifact_only_dirty: artifactOnlyDirty,
|
|
1414
|
-
};
|
|
1415
|
-
candidates.push(candidate);
|
|
1416
|
-
if (!apply) continue;
|
|
1417
|
-
const removeArgs = ['worktree', 'remove'];
|
|
1418
|
-
if (artifactOnlyDirty) removeArgs.push('--force');
|
|
1419
|
-
removeArgs.push(wt.path);
|
|
1420
|
-
const removedResult = runGit(removeArgs, { cwd: root, check: false });
|
|
1421
|
-
if (removedResult.status === 0) {
|
|
1422
|
-
removed.push(candidate);
|
|
1423
|
-
recordWorktreeRemoval(root, { path: wt.path, branch: wt.branch, by, reason: candidate.reason });
|
|
1424
|
-
} else {
|
|
1425
|
-
kept.push({ ...item, reason: 'remove_failed', error: (removedResult.stderr || removedResult.stdout || '').trim() });
|
|
1426
|
-
}
|
|
1479
|
+
pruneCachesForKept(wtPath);
|
|
1480
|
+
kept.push({ ...item, reason: wt.branch === 'detached' ? 'detached_unmerged' : 'unmerged' });
|
|
1427
1481
|
continue;
|
|
1428
1482
|
}
|
|
1429
1483
|
if (worktreeWithinReapGrace(wtPath)) {
|
|
1430
1484
|
kept.push({ ...item, reason: 'fresh_worktree_grace' });
|
|
1431
1485
|
continue;
|
|
1432
1486
|
}
|
|
1433
|
-
|
|
1487
|
+
if (!originBaseIsCurrent(root, base)) {
|
|
1488
|
+
kept.push({ ...item, reason: 'base_unverified' });
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
let real;
|
|
1492
|
+
try { real = fs.realpathSync(wtPath); } catch { real = ''; }
|
|
1493
|
+
if (!real || real !== wtPath) {
|
|
1494
|
+
kept.push({ ...item, reason: 'path_not_canonical' });
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const measured = spawnSync('du', ['-sk', wtPath], { encoding: 'utf8', timeout: 30000, killSignal: 'SIGKILL' });
|
|
1498
|
+
const sizeMatch = measured.status === 0 && !String(measured.stderr || '').trim() && String(measured.stdout || '').match(/^(\d+)\s/);
|
|
1499
|
+
const kib = sizeMatch ? Number(sizeMatch[1]) : 0;
|
|
1500
|
+
if (!kib || cacheBudget.count >= 20 || cacheBudget.kib + kib > 10 * 1024 * 1024) {
|
|
1501
|
+
kept.push({ ...item, reason: 'size_unknown_or_cleanup_limit' });
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
const candidate = { ...item, reason: 'merged_into_base', size_kib: kib };
|
|
1434
1505
|
candidates.push(candidate);
|
|
1506
|
+
cacheBudget.count += 1;
|
|
1507
|
+
cacheBudget.kib += kib;
|
|
1435
1508
|
if (!apply) continue;
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
const
|
|
1509
|
+
const freshActive = Array.isArray(activeCwdsOverride) ? activeCwds : listActiveProcessPaths();
|
|
1510
|
+
const freshCounts = statusCounts(wt.path);
|
|
1511
|
+
const freshIgnored = runGit(['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'], { cwd: wt.path, check: false });
|
|
1512
|
+
const freshSize = spawnSync('du', ['-sk', wtPath], { encoding: 'utf8', timeout: 30000, killSignal: 'SIGKILL' });
|
|
1513
|
+
const freshMatch = freshSize.status === 0 && !String(freshSize.stderr || '').trim() && String(freshSize.stdout || '').match(/^(\d+)\s/);
|
|
1514
|
+
const gitdir = runGit(['rev-parse', '--absolute-git-dir'], { cwd: wtPath, check: false });
|
|
1515
|
+
const freshHead = runGit(['rev-parse', 'HEAD'], { cwd: wtPath, check: false });
|
|
1516
|
+
if (!Array.isArray(freshActive) || worktreeHasActiveProcess(wtPath, freshActive)
|
|
1517
|
+
|| !freshCounts || freshCounts.staged || freshCounts.unstaged || freshCounts.untracked
|
|
1518
|
+
|| freshIgnored.status !== 0 || freshIgnored.stdout || !freshMatch || Number(freshMatch[1]) !== kib
|
|
1519
|
+
|| gitdir.status !== 0 || !gitdir.stdout.trim() || fs.existsSync(path.join(gitdir.stdout.trim(), 'locked'))
|
|
1520
|
+
|| freshHead.status !== 0 || String(freshHead.stdout || '').trim() !== wt.head
|
|
1521
|
+
|| runGit(['merge-base', '--is-ancestor', wt.head, base], { cwd: root, check: false }).status !== 0
|
|
1522
|
+
|| !originBaseIsCurrent(root, base)
|
|
1523
|
+
|| canonicalPath(wtPath) !== wtPath) {
|
|
1524
|
+
kept.push({ ...item, reason: 'changed_or_active_during_verification' });
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const removedResult = runGit(['worktree', 'remove', wt.path], { cwd: root, check: false });
|
|
1440
1528
|
if (removedResult.status === 0) {
|
|
1441
1529
|
removed.push(candidate);
|
|
1442
1530
|
recordWorktreeRemoval(root, { path: wt.path, branch: wt.branch, by, reason: candidate.reason });
|
|
@@ -1465,10 +1553,13 @@ function cleanup(args) {
|
|
|
1465
1553
|
for (const item of result.apply ? result.removed : result.candidates) {
|
|
1466
1554
|
console.log(`${action}: ${item.path} branch=${item.branch} reason=${item.reason}`);
|
|
1467
1555
|
}
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1556
|
+
const review = result.cacheCandidates.filter((item) => item.reason === 'manual_review_required');
|
|
1557
|
+
const safe = result.apply ? result.cachePruned : result.cacheCandidates.filter((item) => item.reason === 'generated_webpack_cache');
|
|
1558
|
+
console.log(`cache ${result.apply ? 'removed' : 'candidate'}s: ${safe.length}`);
|
|
1559
|
+
for (const item of safe) console.log(`cache: ${item.path} size_kib=${item.kib}`);
|
|
1560
|
+
console.log(`cache review needed: ${review.length}`);
|
|
1561
|
+
for (const item of review) {
|
|
1562
|
+
console.log(`review: ${item.path} reason=${item.reason}`);
|
|
1472
1563
|
}
|
|
1473
1564
|
console.log(`kept: ${result.kept.length}`);
|
|
1474
1565
|
if (!result.apply && result.candidates.length) console.log('next: atris worktree cleanup --apply');
|
|
@@ -1524,7 +1615,7 @@ function help() {
|
|
|
1524
1615
|
console.log(' atris worktree status');
|
|
1525
1616
|
console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
|
|
1526
1617
|
console.log(' atris worktree prune [--apply]');
|
|
1527
|
-
console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master] reap completed checkouts and
|
|
1618
|
+
console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master] reap completed checkouts and verified webpack caches');
|
|
1528
1619
|
}
|
|
1529
1620
|
|
|
1530
1621
|
function worktreeCommand(args = []) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The Atris Rating: one number, 25 to 99, for how much real work a teammate
|
|
4
|
+
// finishes. 99 means 99 of every 100. Each time a teammate halves its misses
|
|
5
|
+
// it gains about 6 points, so the top is the hardest climb: going from 96 to
|
|
6
|
+
// 99 percent finished is worth about 12.
|
|
7
|
+
//
|
|
8
|
+
// One scale rates two things. An engine gets its number from the combine
|
|
9
|
+
// below: real tasks graded by the tests that shipped with them. A member
|
|
10
|
+
// gets its number from the jobs it lands in its own workspace (gm-season).
|
|
11
|
+
|
|
12
|
+
const { modelLabel } = require('./roster-models');
|
|
13
|
+
|
|
14
|
+
const TOP = 99;
|
|
15
|
+
const FLOOR = 25;
|
|
16
|
+
const STEP = 6;
|
|
17
|
+
|
|
18
|
+
// Every rating starts from half a finish and half a miss, so a short perfect
|
|
19
|
+
// run cannot reach the top: 3 of 3 rates 76, and it takes about 60 in a row
|
|
20
|
+
// to rate 99. null when nothing has been tried.
|
|
21
|
+
function atrisRating(finished, tries) {
|
|
22
|
+
const all = Number(tries);
|
|
23
|
+
const done = Number(finished);
|
|
24
|
+
if (!Number.isFinite(all) || all <= 0 || !Number.isFinite(done)) return null;
|
|
25
|
+
const share = (Math.min(Math.max(done, 0), all) + 0.5) / (all + 1);
|
|
26
|
+
const raw = TOP - STEP * (Math.log2(TOP) - Math.log2(share / (1 - share)));
|
|
27
|
+
return Math.max(FLOOR, Math.min(TOP, Math.round(raw)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The combine: Loop Bench, tasks from the atris repos (javascript from the
|
|
31
|
+
// CLI, python from the backend), each graded by the hidden tests that shipped
|
|
32
|
+
// with its fix, run in Claude Code on the builder prompt Atris kept. unseen
|
|
33
|
+
// tasks were held out of every prompt change, so the prompt was never tuned
|
|
34
|
+
// on them. finished adds each task's pass rate over its tries; time and price
|
|
35
|
+
// count each task once too. broke counts tries that passed their own tests
|
|
36
|
+
// but broke another one. The file is what evals/loopbench/combine.py in
|
|
37
|
+
// atrisos-backend prints, copied as is, so re-rating is one copy.
|
|
38
|
+
function freeze(value) {
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
for (const inner of Object.values(value)) freeze(inner);
|
|
41
|
+
Object.freeze(value);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const COMBINE = freeze(require('./atris-rating-combine.json'));
|
|
47
|
+
|
|
48
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
|
|
49
|
+
'August', 'September', 'October', 'November', 'December'];
|
|
50
|
+
|
|
51
|
+
// "2026-10-29" -> "October 2026"
|
|
52
|
+
function monthOf(date) {
|
|
53
|
+
const [year, month] = String(date || '').split('-').map(Number);
|
|
54
|
+
return year && month >= 1 && month <= 12 ? `${MONTHS[month - 1]} ${year}` : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const RATED = monthOf(COMBINE.rated);
|
|
58
|
+
|
|
59
|
+
// One engine's card: the overall and four parts, each on the same scale.
|
|
60
|
+
function engineCard(row) {
|
|
61
|
+
return {
|
|
62
|
+
model: row.model,
|
|
63
|
+
label: modelLabel(row.model),
|
|
64
|
+
ovr: atrisRating(row.finished, row.tasks),
|
|
65
|
+
finished: row.finished,
|
|
66
|
+
tasks: row.tasks,
|
|
67
|
+
python: atrisRating(row.python.finished, row.python.tasks),
|
|
68
|
+
javascript: atrisRating(row.javascript.finished, row.javascript.tasks),
|
|
69
|
+
unseen: atrisRating(row.unseen.finished, row.unseen.tasks),
|
|
70
|
+
clean: atrisRating(row.tries - row.broke, row.tries),
|
|
71
|
+
parts: { python: row.python, javascript: row.javascript, unseen: row.unseen },
|
|
72
|
+
tries: row.tries,
|
|
73
|
+
broke: row.broke,
|
|
74
|
+
minutes_cap: row.minutes_cap,
|
|
75
|
+
median_minutes: row.median_minutes,
|
|
76
|
+
cost_per_finish: row.cost_per_finish,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Every rated engine, best first.
|
|
81
|
+
function combineCards() {
|
|
82
|
+
return COMBINE.engines.map(engineCard).sort((a, b) => b.ovr - a.ovr || a.label.localeCompare(b.label));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The card for a model id, a label, or a family word that names one engine;
|
|
86
|
+
// null for a model the combine has not rated.
|
|
87
|
+
function combineCardFor(name) {
|
|
88
|
+
const text = String(name || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
89
|
+
if (!text) return null;
|
|
90
|
+
const cards = combineCards();
|
|
91
|
+
const exact = cards.find((card) => card.model === text || card.label === text);
|
|
92
|
+
if (exact) return exact;
|
|
93
|
+
const family = cards.filter((card) => card.label.split(' ')[0] === text);
|
|
94
|
+
return family.length === 1 ? family[0] : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function finishedText(value) {
|
|
98
|
+
return Number.isInteger(value) ? String(value) : String(Math.round(value * 10) / 10);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function priceText(dollars) {
|
|
102
|
+
if (typeof dollars !== 'number') return '-';
|
|
103
|
+
return dollars < 1 ? `${Math.round(dollars * 100)}¢` : `$${dollars.toFixed(2)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function minutesText(minutes) {
|
|
107
|
+
return typeof minutes === 'number' ? `${minutes.toFixed(1)} min` : '-';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const SCALE_LINES = Object.freeze([
|
|
111
|
+
'how much real work a teammate finishes, 25 to 99. 99 means 99 of every 100.',
|
|
112
|
+
'each halving of misses is worth about 6 points, so the top is the hardest climb.',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// The league table: every rated engine, best first.
|
|
116
|
+
function renderCombine(cards = combineCards()) {
|
|
117
|
+
const caps = [...new Set(cards.map((card) => card.minutes_cap))];
|
|
118
|
+
const capLine = caps.length > 1
|
|
119
|
+
? cards.filter((card) => card.minutes_cap !== Math.min(...caps)).map((card) => `${card.label} gets ${card.minutes_cap} minutes a task`).join(', ') + `, the others ${Math.min(...caps)}.`
|
|
120
|
+
: `every engine gets ${caps[0]} minutes a task.`;
|
|
121
|
+
const lines = [
|
|
122
|
+
'atris rating',
|
|
123
|
+
...SCALE_LINES,
|
|
124
|
+
'',
|
|
125
|
+
`the combine: ${COMBINE.tasks} atris tasks, each graded by the tests that shipped with it`,
|
|
126
|
+
'',
|
|
127
|
+
` ${'ovr'.padEnd(5)}${'engine'.padEnd(12)}${'finished'.padEnd(12)}${'py'.padEnd(4)}${'js'.padEnd(4)}${'unseen'.padEnd(8)}${'clean'.padEnd(7)}${'time'.padEnd(9)}price`,
|
|
128
|
+
];
|
|
129
|
+
for (const card of cards) {
|
|
130
|
+
lines.push(` ${String(card.ovr).padEnd(5)}${card.label.padEnd(12)}${`${finishedText(card.finished)} of ${card.tasks}`.padEnd(12)}${String(card.python).padEnd(4)}${String(card.javascript).padEnd(4)}${String(card.unseen).padEnd(8)}${String(card.clean).padEnd(7)}${minutesText(card.median_minutes).padEnd(9)}${priceText(card.cost_per_finish)}`);
|
|
131
|
+
}
|
|
132
|
+
const top = cards[0];
|
|
133
|
+
lines.push(
|
|
134
|
+
'',
|
|
135
|
+
top && top.ovr >= TOP ? `${top.label} is a 99.` : 'no engine is a 99 yet.',
|
|
136
|
+
'py, js: python and javascript tasks. unseen: ones the prompt was never tuned on.',
|
|
137
|
+
'clean: tries that broke no other test. time: median to finish.',
|
|
138
|
+
'price: per finished task, pay-per-use.',
|
|
139
|
+
capLine,
|
|
140
|
+
`${COMBINE.bench}, ${RATED}. one engine: atris rating <engine>`,
|
|
141
|
+
);
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// One engine's card.
|
|
146
|
+
function renderCard(card) {
|
|
147
|
+
const head = `${card.label}, builder`;
|
|
148
|
+
const part = (name, value, text) => ` ${name.padEnd(13)}${String(value).padEnd(5)}${text}`;
|
|
149
|
+
return [
|
|
150
|
+
`${head}${' '.repeat(Math.max(2, 44 - head.length))}${card.ovr} ovr`,
|
|
151
|
+
'',
|
|
152
|
+
part('python', card.python, `${finishedText(card.parts.python.finished)} of ${card.parts.python.tasks} tasks`),
|
|
153
|
+
part('javascript', card.javascript, `${finishedText(card.parts.javascript.finished)} of ${card.parts.javascript.tasks} tasks`),
|
|
154
|
+
part('unseen', card.unseen, `${finishedText(card.parts.unseen.finished)} of ${card.parts.unseen.tasks} tasks the Atris prompt was never tuned on`),
|
|
155
|
+
part('clean', card.clean, `broke another test on ${card.broke} of ${card.tries} tries`),
|
|
156
|
+
'',
|
|
157
|
+
` finished ${finishedText(card.finished)} of ${card.tasks} tasks, ${minutesText(card.median_minutes)} median, ${card.minutes_cap} minute cap`,
|
|
158
|
+
` ${priceText(card.cost_per_finish)} per finished task on pay-per-use`,
|
|
159
|
+
` ${COMBINE.bench}, ${RATED}, ${card.tries} tries graded by each task's shipped tests`,
|
|
160
|
+
].join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// What the boot team line adds after a build engine, like " (86 ovr)", or ''
|
|
164
|
+
// for a model the combine has not rated.
|
|
165
|
+
function bootRatingText(model) {
|
|
166
|
+
const card = model ? combineCards().find((c) => c.model === String(model).trim()) : null;
|
|
167
|
+
return card ? ` (${card.ovr} ovr)` : '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
COMBINE,
|
|
172
|
+
SCALE_LINES,
|
|
173
|
+
atrisRating,
|
|
174
|
+
bootRatingText,
|
|
175
|
+
combineCardFor,
|
|
176
|
+
combineCards,
|
|
177
|
+
renderCard,
|
|
178
|
+
renderCombine,
|
|
179
|
+
};
|