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/lib/fleet.js
CHANGED
|
@@ -3208,14 +3208,27 @@ async function runFleetFlight({
|
|
|
3208
3208
|
// when the operator explicitly wants launcher-HEAD.
|
|
3209
3209
|
const startBaseArgs = ['--base', landTarget];
|
|
3210
3210
|
const prepared = [];
|
|
3211
|
+
const leaseDb = require('./task-db');
|
|
3212
|
+
const leaseConnection = leaseDb.open();
|
|
3213
|
+
const leased = new Map();
|
|
3214
|
+
const leaseTimer = setInterval(() => {
|
|
3215
|
+
for (const [id, holder] of leased) leaseDb.renewLease(leaseConnection, { id, holder, pid: process.pid });
|
|
3216
|
+
}, 5 * 60000);
|
|
3217
|
+
leaseTimer.unref();
|
|
3218
|
+
try {
|
|
3211
3219
|
for (const entry of staffed) {
|
|
3212
|
-
const claimed = cli(['task', 'claim', String(entry.task.display_id), '--as', `fleet-${entry.engine}
|
|
3220
|
+
const claimed = cli(['task', 'claim', String(entry.task.display_id), '--as', `fleet-${entry.engine}`, '--lease-minutes', '60', '--lease-pid', String(process.pid)]);
|
|
3213
3221
|
if (!claimed || claimed.status !== 0) {
|
|
3214
3222
|
const detail = String(claimed && (claimed.stderr || claimed.stdout) || 'claim failed').trim().slice(0, 300);
|
|
3215
3223
|
flight.paused.push({ task: entry.task.display_id, engine: entry.engine, stage: 'claim', detail });
|
|
3216
3224
|
log(` ${entry.engine.padEnd(8)} ⏸ paused ${entry.task.display_id} at claim${detail ? `, ${detail}` : ''}`);
|
|
3217
3225
|
continue;
|
|
3218
3226
|
}
|
|
3227
|
+
const leaseId = entry.task.id || entry.task.display_id;
|
|
3228
|
+
const holder = `fleet-${entry.engine}`;
|
|
3229
|
+
const leaseSet = leaseDb.setLease(leaseConnection, { id: leaseId, holder });
|
|
3230
|
+
if (!leaseSet && (!ownCli || leaseDb.getTask(leaseConnection, leaseId))) throw new Error('could not set claim lease');
|
|
3231
|
+
if (leaseSet) leased.set(leaseId, holder);
|
|
3219
3232
|
const started = cli(['worktree', 'start', '--agent', entry.engine, '--task', `fleet-${String(entry.task.display_id).toLowerCase()}`, ...startBaseArgs]);
|
|
3220
3233
|
const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
|
|
3221
3234
|
if (!wt) {
|
|
@@ -3340,6 +3353,13 @@ async function runFleetFlight({
|
|
|
3340
3353
|
log(` flight over: ${flight.landed.length} landed, ${flight.paused.length} paused · receipt: ${path.relative(root, flight.receipt)}`);
|
|
3341
3354
|
log('');
|
|
3342
3355
|
return flight;
|
|
3356
|
+
} finally {
|
|
3357
|
+
clearInterval(leaseTimer);
|
|
3358
|
+
for (const [id, holder] of leased) {
|
|
3359
|
+
const row = leaseDb.getTask(leaseConnection, id);
|
|
3360
|
+
if (row && row.status !== 'claimed') leaseDb.clearLease(leaseConnection, { id, holder });
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3343
3363
|
}
|
|
3344
3364
|
|
|
3345
3365
|
// ---------------------------------------------------------------------------
|
|
@@ -3474,6 +3494,9 @@ async function runDispatchFlight({
|
|
|
3474
3494
|
}
|
|
3475
3495
|
|
|
3476
3496
|
const cli = ownCli || defaultOwnCli(root);
|
|
3497
|
+
const leaseDb = require('./task-db');
|
|
3498
|
+
const leaseConnection = leaseDb.open();
|
|
3499
|
+
try { leaseDb.expireLeases(leaseConnection); } catch (err) { if (process.env.ATRIS_DEBUG) process.stderr.write(`lease sweep failed: ${String(err?.message ?? err).replace(/[\r\n]+/g, ' ')}\n`); }
|
|
3477
3500
|
// Cut worktrees from, and check self-landing against, the workspace's real
|
|
3478
3501
|
// protected branch, not a hard-coded origin/master (CLI-1298).
|
|
3479
3502
|
const localBase = reviewOnly ? '' : localDispatchBase(root);
|
|
@@ -3517,6 +3540,12 @@ async function runDispatchFlight({
|
|
|
3517
3540
|
// default so rebase-before-ship never replays a launcher feature branch.
|
|
3518
3541
|
const startBaseArgs = landTarget ? ['--base', landTarget] : [];
|
|
3519
3542
|
const prepared = [];
|
|
3543
|
+
const leasedIds = new Set();
|
|
3544
|
+
const leaseTimer = setInterval(() => {
|
|
3545
|
+
for (const id of leasedIds) leaseDb.renewLease(leaseConnection, { id, holder: taskActor, pid: process.pid });
|
|
3546
|
+
}, 5 * 60000);
|
|
3547
|
+
leaseTimer.unref();
|
|
3548
|
+
try {
|
|
3520
3549
|
for (const taskId of ids) {
|
|
3521
3550
|
const task = readTaskById(cli, taskId);
|
|
3522
3551
|
if (!task) {
|
|
@@ -3524,7 +3553,7 @@ async function runDispatchFlight({
|
|
|
3524
3553
|
log(` ✗ ${taskId} not found`);
|
|
3525
3554
|
continue;
|
|
3526
3555
|
}
|
|
3527
|
-
const claimed = cli(['task', 'claim', taskId, '--as', taskActor]);
|
|
3556
|
+
const claimed = cli(['task', 'claim', taskId, '--as', taskActor, '--lease-minutes', '60', '--lease-pid', String(process.pid)]);
|
|
3528
3557
|
if (!claimed || claimed.status !== 0) {
|
|
3529
3558
|
const detail = String(claimed && (claimed.stderr || claimed.stdout) || 'claim failed').trim().slice(0, 300);
|
|
3530
3559
|
flight.paused.push({ task: taskId, stage: 'claim', detail });
|
|
@@ -3532,6 +3561,10 @@ async function runDispatchFlight({
|
|
|
3532
3561
|
continue;
|
|
3533
3562
|
}
|
|
3534
3563
|
try {
|
|
3564
|
+
const leaseId = task.id || taskId;
|
|
3565
|
+
const leased = leaseDb.setLease(leaseConnection, { id: leaseId, holder: taskActor });
|
|
3566
|
+
if (!leased && (!ownCli || leaseDb.getTask(leaseConnection, leaseId))) throw new Error('could not set claim lease');
|
|
3567
|
+
if (leased) leasedIds.add(leaseId);
|
|
3535
3568
|
let landingWorktreePath = '';
|
|
3536
3569
|
let remoteBoundary = null;
|
|
3537
3570
|
if (enforceRemoteBoundary) {
|
|
@@ -4212,4 +4245,11 @@ async function runDispatchFlight({
|
|
|
4212
4245
|
log(` dispatch over: ${completedLabel}, ${flight.paused.length} paused - receipt: ${path.relative(root, flight.receipt)}`);
|
|
4213
4246
|
log('');
|
|
4214
4247
|
return flight;
|
|
4248
|
+
} finally {
|
|
4249
|
+
clearInterval(leaseTimer);
|
|
4250
|
+
for (const id of leasedIds) {
|
|
4251
|
+
const row = leaseDb.getTask(leaseConnection, id);
|
|
4252
|
+
if (row && row.status !== 'claimed') leaseDb.clearLease(leaseConnection, { id, holder: taskActor });
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4215
4255
|
}
|
package/lib/gm-bet.js
CHANGED
|
@@ -4573,8 +4573,8 @@ function createBetDesk(deps) {
|
|
|
4573
4573
|
|
|
4574
4574
|
// The team page: every member on the team, one row each. role is a few
|
|
4575
4575
|
// words from its MEMBER.md. ovr is the board's own rating (atris gm
|
|
4576
|
-
// --board):
|
|
4577
|
-
// last 30 days, "new" with 1 or 2 finished, and two dots with none
|
|
4576
|
+
// --board): the Atris Rating, 25 to 99, of the finished jobs that landed
|
|
4577
|
+
// in the last 30 days, "new" with 1 or 2 finished, and two dots with none
|
|
4578
4578
|
// finished in that window (a first claim alone is not a rating) or no
|
|
4579
4579
|
// task list to read. engine is the tool and the exact model the roster
|
|
4580
4580
|
// gives the member (atris engine roster), two dots when no model is
|
package/lib/gm-season.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const { atrisRating } = require('./atris-rating');
|
|
13
14
|
|
|
14
15
|
const HOUR_MS = 60 * 60 * 1000;
|
|
15
16
|
const DAY_MS = 24 * HOUR_MS;
|
|
@@ -49,17 +50,18 @@ function tally(outcomes) {
|
|
|
49
50
|
return out;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
+
// The Atris Rating of landed over finished (lib/atris-rating.js): 25 to 99,
|
|
54
|
+
// where 99 means 99 of every 100, rookie under 3 finished jobs. This is
|
|
53
55
|
// stricter than the website rule (kept / graded) on purpose: drops and
|
|
54
56
|
// abandoned jobs count.
|
|
55
57
|
function ovr(outcomes) {
|
|
56
58
|
const { landed, finished } = tally(outcomes);
|
|
57
59
|
if (finished < ROOKIE_FINISHED) return null;
|
|
58
|
-
return
|
|
60
|
+
return atrisRating(landed, finished);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// Each member's rating over one window of plays, the board's own numbers:
|
|
62
|
-
// ovr is
|
|
64
|
+
// ovr is 25 to 99, or null for a rookie under 3 finished. A member with no
|
|
63
65
|
// plays in the window is not in the map.
|
|
64
66
|
function memberRatings(outcomes = []) {
|
|
65
67
|
const groups = new Map();
|
|
@@ -320,6 +322,12 @@ function outcomeFromEvent(row) {
|
|
|
320
322
|
} else if (row.event_type === 'claim_released') {
|
|
321
323
|
kind = 'dropped';
|
|
322
324
|
member = payload.previous_claimed_by || payload.released_by || row.actor;
|
|
325
|
+
} else if (row.event_type === 'claim_expired') {
|
|
326
|
+
kind = 'dropped';
|
|
327
|
+
member = payload.claimed_by;
|
|
328
|
+
} else if ((row.event_type === 'proof_ready' || row.event_type === 'landing') && hasStructuredMerge(payload)) {
|
|
329
|
+
kind = 'landed';
|
|
330
|
+
member = holder;
|
|
323
331
|
} else if (row.event_type === 'completed' && KEPT_ACTIONS.has(payload.action)) {
|
|
324
332
|
kind = 'landed';
|
|
325
333
|
member = holder;
|
|
@@ -338,6 +346,7 @@ function outcomeFromEvent(row) {
|
|
|
338
346
|
const claimId = kind === 'took'
|
|
339
347
|
? (row.event_id || `${row.task_id}:${row.created_at}`)
|
|
340
348
|
: (row.claim_event_id || `${row.task_id}:before`);
|
|
349
|
+
const leaseUntil = typeof payload.lease_until === 'number' ? payload.lease_until : typeof payload.lease_until === 'string' ? Date.parse(payload.lease_until) : NaN;
|
|
341
350
|
return {
|
|
342
351
|
event_id: row.event_id || null,
|
|
343
352
|
claim_id: claimId,
|
|
@@ -345,14 +354,56 @@ function outcomeFromEvent(row) {
|
|
|
345
354
|
member: String(member),
|
|
346
355
|
title: row.title || '',
|
|
347
356
|
kind,
|
|
348
|
-
at: Number(row.created_at),
|
|
357
|
+
at: row.event_type === 'claim_expired' && Number.isFinite(leaseUntil) ? leaseUntil : Number(row.created_at),
|
|
358
|
+
reason: kind !== 'took' && kind !== 'landed' ? (typeof payload.reason === 'string' && payload.reason.trim() || typeof payload.note === 'string' && payload.note.trim() || null) : null,
|
|
349
359
|
claimed_at: claimedAt,
|
|
350
360
|
landed_by: kind === 'landed' ? landedBy(row, payload) : null,
|
|
351
361
|
};
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
function
|
|
355
|
-
|
|
364
|
+
function hasStructuredMerge(payload) {
|
|
365
|
+
const objects = [payload, payload.landing, payload.pull_request, payload.pr].filter(x => x && typeof x === 'object' && !Array.isArray(x));
|
|
366
|
+
return objects.some(x => {
|
|
367
|
+
const commit = x.merge_commit || x.merge_commit_sha || x.merge_sha;
|
|
368
|
+
return (typeof commit === 'string' && /^[0-9a-f]{7,64}$/i.test(commit)) ||
|
|
369
|
+
x.merged === true || Boolean(x.merged_at) ||
|
|
370
|
+
((typeof x.merged_pr === 'string' && x.merged_pr.trim() !== '') || (typeof x.merged_pr === 'number' && x.merged_pr > 0) || (x.merged_pr && typeof x.merged_pr === 'object' && (x.merged_pr.merged === true || x.merged_pr.state === 'merged' || x.merged_pr.merged_at))) ||
|
|
371
|
+
((x.state === 'merged' || x.status === 'merged') && (x.number != null || x.url != null));
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function outcomesFromEvents(rows, since = -Infinity) {
|
|
376
|
+
const ordered = [...(rows || [])].sort((a, b) => String(a.task_id).localeCompare(String(b.task_id)) ||
|
|
377
|
+
(a.version != null && b.version != null ? Number(a.version) - Number(b.version) : Number(a.created_at) - Number(b.created_at)));
|
|
378
|
+
const plays = [];
|
|
379
|
+
const finished = new Map();
|
|
380
|
+
const closed = new Set();
|
|
381
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
382
|
+
const row = ordered[i];
|
|
383
|
+
const play = outcomeFromEvent(row);
|
|
384
|
+
const key = `${row.task_id}:${row.claim_event_id || `${row.task_id}:before`}`;
|
|
385
|
+
const ended = ['claim_released', 'claim_expired', 'completed', 'reopened', 'archived'].includes(row.event_type);
|
|
386
|
+
if (!play) { if (ended) closed.add(key); continue; }
|
|
387
|
+
if (play.kind === 'took') { plays.push(play); continue; }
|
|
388
|
+
if (row.event_type === 'claim_released') {
|
|
389
|
+
let next = null;
|
|
390
|
+
for (let j = i + 1; j < ordered.length && ordered[j].task_id === row.task_id; j++) {
|
|
391
|
+
if (ordered[j].event_type === 'claimed') { next = ordered[j]; break; }
|
|
392
|
+
}
|
|
393
|
+
const nextMember = next && (next.actor || (parseJson(next.payload) || {}).claimed_by);
|
|
394
|
+
if (next && nextMember &&
|
|
395
|
+
Number(next.created_at) >= Number(row.created_at) && Number(next.created_at) - Number(row.created_at) <= 120000 &&
|
|
396
|
+
String(nextMember) !== play.member) { closed.add(key); continue; }
|
|
397
|
+
}
|
|
398
|
+
const prior = finished.get(key);
|
|
399
|
+
if (!prior && !closed.has(key)) { finished.set(key, play); plays.push(play); }
|
|
400
|
+
else if (prior && !closed.has(key) && play.kind === 'landed' && prior.kind !== 'landed') {
|
|
401
|
+
plays[plays.indexOf(prior)] = play;
|
|
402
|
+
finished.set(key, play);
|
|
403
|
+
}
|
|
404
|
+
if (ended) closed.add(key);
|
|
405
|
+
}
|
|
406
|
+
return plays.filter(play => play.at >= since).sort(byTimeAsc);
|
|
356
407
|
}
|
|
357
408
|
|
|
358
409
|
function emptySeason(workspaceRoot, now) {
|
|
@@ -465,7 +516,7 @@ function missionLastAt(mission) {
|
|
|
465
516
|
// right holder. `extraWhere` narrows it (one workspace, or none for the league).
|
|
466
517
|
function playEventsSql(extraWhere = '') {
|
|
467
518
|
return `
|
|
468
|
-
SELECT e.event_id, e.task_id, e.workspace_root, e.event_type, e.actor, e.payload, e.created_at,
|
|
519
|
+
SELECT e.event_id, e.task_id, e.workspace_root, e.event_type, e.actor, e.payload, e.created_at, e.version,
|
|
469
520
|
t.title, t.claimed_by, t.claimed_at,
|
|
470
521
|
c.event_id AS claim_event_id, c.created_at AS claim_event_at, c.actor AS claim_actor,
|
|
471
522
|
CASE WHEN e.event_type = 'archived' AND c.event_id IS NOT NULL AND NOT EXISTS (
|
|
@@ -483,8 +534,8 @@ function playEventsSql(extraWhere = '') {
|
|
|
483
534
|
SELECT p.event_id FROM task_events p
|
|
484
535
|
WHERE p.task_id = e.task_id AND p.event_type = 'claimed' AND p.version < e.version
|
|
485
536
|
ORDER BY p.version DESC LIMIT 1)
|
|
486
|
-
WHERE e.created_at >= ?
|
|
487
|
-
AND e.event_type IN ('claimed', 'claim_released', 'completed', 'revision_requested', 'reopened', 'archived')
|
|
537
|
+
WHERE EXISTS (SELECT 1 FROM task_events recent WHERE recent.task_id = e.task_id AND recent.created_at >= ?)
|
|
538
|
+
AND e.event_type IN ('claimed', 'claim_released', 'claim_expired', 'proof_ready', 'landing', 'completed', 'revision_requested', 'reopened', 'archived')
|
|
488
539
|
${extraWhere}
|
|
489
540
|
ORDER BY e.created_at ASC`;
|
|
490
541
|
}
|
|
@@ -529,10 +580,10 @@ function loadSeason(workspaceRoot, { now = Date.now(), days = SEASON_DAYS, dbPat
|
|
|
529
580
|
db = new DatabaseSync(target, { readOnly: true });
|
|
530
581
|
const since = now - days * DAY_MS;
|
|
531
582
|
const playsSince = db.prepare(playEventsSql('AND e.workspace_root = ?'));
|
|
532
|
-
season.outcomes = outcomesFromEvents(playsSince.all(since, workspaceRoot));
|
|
583
|
+
season.outcomes = outcomesFromEvents(playsSince.all(since, workspaceRoot), since);
|
|
533
584
|
const seenAt = Number(season.last_seen_at);
|
|
534
585
|
season.recap_outcomes = Number.isFinite(seenAt) && seenAt > 0 && seenAt < since
|
|
535
|
-
? outcomesFromEvents(playsSince.all(seenAt, workspaceRoot))
|
|
586
|
+
? outcomesFromEvents(playsSince.all(seenAt, workspaceRoot), seenAt)
|
|
536
587
|
: null;
|
|
537
588
|
|
|
538
589
|
const taskRows = db.prepare(`
|
|
@@ -586,7 +637,7 @@ function loadTargetView(db, workspaceRoot, { now, since, playsSince, outcomes, c
|
|
|
586
637
|
const gmTarget = require('./gm-target');
|
|
587
638
|
const store = gmTarget.readTargets(workspaceRoot, dir ? { dir } : {});
|
|
588
639
|
const from = gmTarget.historyStart(store, now);
|
|
589
|
-
const history = db && from < since ? outcomesFromEvents(playsSince.all(from, workspaceRoot)) : outcomes;
|
|
640
|
+
const history = db && from < since ? outcomesFromEvents(playsSince.all(from, workspaceRoot), from) : outcomes;
|
|
590
641
|
const hasPastWeeks = Object.keys(store.weeks).some(k => k < gmTarget.weekKey(now));
|
|
591
642
|
return {
|
|
592
643
|
has_target: Object.keys(store.weeks).length > 0,
|
|
@@ -756,7 +807,7 @@ function loadLeague({ now = Date.now(), days = SEASON_DAYS, dbPath, isClub = isC
|
|
|
756
807
|
}
|
|
757
808
|
const gmTarget = require('./gm-target');
|
|
758
809
|
league.clubs = [...byRoot.values()].map((club) => {
|
|
759
|
-
const outcomes = outcomesFromEvents(club.events);
|
|
810
|
+
const outcomes = outcomesFromEvents(club.events, since);
|
|
760
811
|
const store = gmTarget.readTargets(club.root, targetsDir ? { dir: targetsDir } : {});
|
|
761
812
|
return {
|
|
762
813
|
root: club.root,
|
|
@@ -1109,11 +1160,9 @@ function loadReplay({ workspaceRoot = null, since, now = Date.now(), dbPath, lea
|
|
|
1109
1160
|
// same unique labels as the league, so /one/app and /two/app stay apart
|
|
1110
1161
|
const names = clubNames([...new Set(kept.map(row => row.workspace_root))]);
|
|
1111
1162
|
const clubOf = root => names.get(root) || path.basename(root);
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
.
|
|
1115
|
-
.map(x => ({ ...x.play, club: clubOf(x.row.workspace_root) }))
|
|
1116
|
-
.sort(byTimeAsc);
|
|
1163
|
+
const rootByTask = new Map(kept.map(row => [row.task_id, row.workspace_root]));
|
|
1164
|
+
out.plays = outcomesFromEvents(kept, since).filter(play => play.at <= now)
|
|
1165
|
+
.map(play => ({ ...play, club: clubOf(rootByTask.get(play.task_id)) }));
|
|
1117
1166
|
const claimed = db.prepare(`
|
|
1118
1167
|
SELECT id, workspace_root, claimed_by, claimed_at, updated_at,
|
|
1119
1168
|
(SELECT MAX(e.created_at) FROM task_events e WHERE e.task_id = tasks.id) AS last_event_at
|
package/lib/known-commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'idea', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '99', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'sync-checkout', 'zen',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
7
7
|
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
|
|
9
9
|
|
package/lib/task-db.js
CHANGED
|
@@ -580,7 +580,7 @@ function workspaceHead(workspaceRoot) {
|
|
|
580
580
|
// Atomic claim. Returns { claimed: true, row } only if THIS call won the row.
|
|
581
581
|
// Race-safe via single UPDATE with WHERE status='open' guard. SQLite serializes
|
|
582
582
|
// writes; busy_timeout absorbs contention. Caller must check `.claimed`.
|
|
583
|
-
function claimTask(db, { id, claimedBy }) {
|
|
583
|
+
function claimTask(db, { id, claimedBy, leaseMinutes, leasePid }) {
|
|
584
584
|
if (!id) throw new Error('id required');
|
|
585
585
|
if (!claimedBy) throw new Error('claimedBy required');
|
|
586
586
|
const now = Date.now();
|
|
@@ -589,11 +589,13 @@ function claimTask(db, { id, claimedBy }) {
|
|
|
589
589
|
SET status = 'claimed',
|
|
590
590
|
claimed_by = ?,
|
|
591
591
|
claimed_at = ?,
|
|
592
|
-
updated_at =
|
|
592
|
+
updated_at = ?,
|
|
593
|
+
metadata = CASE WHEN ? IS NULL THEN metadata ELSE json_set(COALESCE(metadata, '{}'), '$.lease', json(?)) END
|
|
593
594
|
WHERE id = ?
|
|
594
595
|
AND status = 'open'
|
|
595
596
|
`);
|
|
596
|
-
const
|
|
597
|
+
const lease = leaseMinutes == null ? null : JSON.stringify({ until: new Date(now + leaseMinutes * 60000).toISOString(), minutes: leaseMinutes, holder: claimedBy, pid: leasePid });
|
|
598
|
+
const result = withBusyRetry(() => stmt.run(claimedBy, now, now, lease, lease, id));
|
|
597
599
|
if (result.changes === 1) {
|
|
598
600
|
let row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
599
601
|
// First claim wins: a re-claim after a revision keeps the original start.
|
|
@@ -621,6 +623,106 @@ function claimTask(db, { id, claimedBy }) {
|
|
|
621
623
|
return { claimed: false, reason: 'already_' + row.status, claimed_by: row.claimed_by };
|
|
622
624
|
}
|
|
623
625
|
|
|
626
|
+
// Lease updates are guarded by the holder and do not create activity events.
|
|
627
|
+
function setLease(db, { id, holder, minutes = 60, pid = process.pid, now = Date.now() }) {
|
|
628
|
+
const raw = db.prepare('SELECT metadata FROM tasks WHERE id = ?').get(id);
|
|
629
|
+
const row = getTask(db, id);
|
|
630
|
+
if (!row || row.metadata_unreadable || row.status !== 'claimed' || row.claimed_by !== holder) return false;
|
|
631
|
+
const metadata = { ...(row.metadata || {}), lease: { until: new Date(Number(now) + minutes * 60000).toISOString(), minutes, holder, pid } };
|
|
632
|
+
return withBusyRetry(() => db.prepare("UPDATE tasks SET metadata = ? WHERE id = ? AND status = 'claimed' AND claimed_by = ? AND metadata IS ?")
|
|
633
|
+
.run(JSON.stringify(metadata), id, holder, raw.metadata)).changes === 1;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renewLease(db, options) {
|
|
637
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(options.id);
|
|
638
|
+
const metadata = row && row.metadata ? safeJSON(row.metadata) : null;
|
|
639
|
+
const lease = metadata && metadata.lease;
|
|
640
|
+
if (!row || row.status !== 'claimed' || row.claimed_by !== options.holder || !lease || lease.holder !== options.holder || (options.pid != null && lease.pid !== options.pid)) return false;
|
|
641
|
+
const minutes = options.minutes ?? lease.minutes;
|
|
642
|
+
const now = options.now ?? Date.now();
|
|
643
|
+
const updated = { ...metadata, lease: { until: new Date(Number(now) + minutes * 60000).toISOString(), minutes, holder: options.holder, pid: options.pid ?? lease.pid } };
|
|
644
|
+
return withBusyRetry(() => db.prepare("UPDATE tasks SET metadata = ? WHERE id = ? AND status = 'claimed' AND claimed_by = ? AND claimed_at IS ? AND metadata IS ?")
|
|
645
|
+
.run(JSON.stringify(updated), row.id, row.claimed_by, row.claimed_at, row.metadata)).changes === 1;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function clearLease(db, { id, holder }) {
|
|
649
|
+
const raw = db.prepare('SELECT metadata FROM tasks WHERE id = ?').get(id);
|
|
650
|
+
const row = getTask(db, id);
|
|
651
|
+
if (!row || !row.metadata || !row.metadata.lease || row.metadata.lease.holder !== holder) return false;
|
|
652
|
+
const metadata = { ...row.metadata };
|
|
653
|
+
delete metadata.lease;
|
|
654
|
+
return withBusyRetry(() => db.prepare('UPDATE tasks SET metadata = ? WHERE id = ? AND metadata IS ?')
|
|
655
|
+
.run(JSON.stringify(metadata), id, raw.metadata)).changes === 1;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function pidAlive(pid) {
|
|
659
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err.code === 'EPERM'; }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function expireLeases(db, { now = Date.now(), staleDays, claimer, dryRun = false, isAlive = pidAlive } = {}) {
|
|
663
|
+
const timestamp = now instanceof Date ? now.getTime() : typeof now === 'string' ? Date.parse(now) : Number(now);
|
|
664
|
+
if (!Number.isFinite(timestamp)) throw new Error('invalid now');
|
|
665
|
+
if (staleDays != null && (!Number.isFinite(Number(staleDays)) || Number(staleDays) < 0)) throw new Error('invalid stale days');
|
|
666
|
+
const rows = db.prepare(`SELECT t.*, (SELECT MAX(created_at) FROM task_events e WHERE e.task_id = t.id) AS last_seen_at FROM tasks t WHERE t.status = 'claimed'`).all();
|
|
667
|
+
const expired = [];
|
|
668
|
+
for (const row of rows) {
|
|
669
|
+
if (claimer && row.claimed_by !== claimer) continue;
|
|
670
|
+
const metadata = row.metadata ? safeJSON(row.metadata) : null;
|
|
671
|
+
const lease = metadata && metadata.lease;
|
|
672
|
+
const until = lease && (typeof lease.until === 'number' ? lease.until : Date.parse(lease.until));
|
|
673
|
+
let reason;
|
|
674
|
+
let leaseUntil;
|
|
675
|
+
if (lease && Number.isFinite(until) && until < timestamp) {
|
|
676
|
+
if (timestamp - until <= 12 * 3600000 && isAlive(lease.pid)) continue;
|
|
677
|
+
reason = `worker stopped without reporting back within ${lease.minutes ?? 60} minutes`;
|
|
678
|
+
leaseUntil = lease.until;
|
|
679
|
+
} else if (!lease && staleDays != null && row.last_seen_at != null && row.last_seen_at < timestamp - Number(staleDays) * 86400000 && (claimer ? row.claimed_by === claimer : String(row.claimed_by || '').startsWith('fleet-'))) {
|
|
680
|
+
reason = `stranded with no activity for over ${staleDays} days`;
|
|
681
|
+
leaseUntil = new Date(row.last_seen_at + 3600000).toISOString();
|
|
682
|
+
} else continue;
|
|
683
|
+
const lastSeen = lease && Number.isFinite(until) && Number.isFinite(Number(lease.minutes))
|
|
684
|
+
? until - Number(lease.minutes) * 60000 : row.last_seen_at;
|
|
685
|
+
const payload = { claimed_by: row.claimed_by, lease_until: leaseUntil, last_seen_at: lastSeen == null ? null : new Date(lastSeen).toISOString(), reason };
|
|
686
|
+
const item = { id: row.id, title: row.title, ...payload };
|
|
687
|
+
if (dryRun) { expired.push(item); continue; }
|
|
688
|
+
withBusyRetry(() => db.exec('BEGIN IMMEDIATE'));
|
|
689
|
+
try {
|
|
690
|
+
const nextMetadata = metadata && typeof metadata === 'object' ? { ...metadata } : {};
|
|
691
|
+
delete nextMetadata.lease;
|
|
692
|
+
const changed = db.prepare(`UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, updated_at = ?, metadata = ? WHERE id = ? AND status = 'claimed' AND claimed_by = ? AND updated_at = ? AND metadata IS ? AND (SELECT MAX(created_at) FROM task_events WHERE task_id = tasks.id) IS ?`)
|
|
693
|
+
.run(Date.now(), JSON.stringify(nextMetadata), row.id, row.claimed_by, row.updated_at, row.metadata, row.last_seen_at);
|
|
694
|
+
if (changed.changes === 1) {
|
|
695
|
+
appendTaskEvent(db, { taskId: row.id, workspaceRoot: row.workspace_root, actor: row.claimed_by, eventType: 'claim_expired', payload });
|
|
696
|
+
expired.push(item);
|
|
697
|
+
}
|
|
698
|
+
db.exec('COMMIT');
|
|
699
|
+
} catch (err) { db.exec('ROLLBACK'); throw err; }
|
|
700
|
+
}
|
|
701
|
+
return expired;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function previewExpiredLeases(options = {}) {
|
|
705
|
+
const target = getDbPath();
|
|
706
|
+
if (!fs.existsSync(target)) return [];
|
|
707
|
+
const reader = new DatabaseSync(target, { readOnly: true });
|
|
708
|
+
try { return expireLeases(reader, { ...options, dryRun: true }); } finally { reader.close(); }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Avoid creating or rewriting an idle database just to check for expired work.
|
|
712
|
+
function sweepExpiredLeases() {
|
|
713
|
+
try {
|
|
714
|
+
const target = getDbPath();
|
|
715
|
+
if (!fs.existsSync(target)) return [];
|
|
716
|
+
const reader = new DatabaseSync(target, { readOnly: true });
|
|
717
|
+
let candidates;
|
|
718
|
+
try { candidates = expireLeases(reader, { dryRun: true }); } finally { reader.close(); }
|
|
719
|
+
return candidates.length ? expireLeases(open(target)) : [];
|
|
720
|
+
} catch (err) {
|
|
721
|
+
if (process.env.ATRIS_DEBUG) process.stderr.write(`lease sweep failed: ${String(err?.message ?? err).replace(/[\r\n]+/g, ' ')}\n`);
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
624
726
|
function releaseTask(db, { id, actor, expectedUpdatedAt }) {
|
|
625
727
|
if (!id) throw new Error('id required');
|
|
626
728
|
if (!actor) throw new Error('actor required');
|
|
@@ -638,7 +740,8 @@ function releaseTask(db, { id, actor, expectedUpdatedAt }) {
|
|
|
638
740
|
SET status = 'open',
|
|
639
741
|
claimed_by = NULL,
|
|
640
742
|
claimed_at = NULL,
|
|
641
|
-
updated_at =
|
|
743
|
+
updated_at = ?,
|
|
744
|
+
metadata = CASE WHEN json_valid(metadata) THEN json_remove(metadata, '$.lease') ELSE metadata END
|
|
642
745
|
WHERE id = ?
|
|
643
746
|
AND status = 'claimed'
|
|
644
747
|
AND claimed_by = ?
|
|
@@ -952,6 +1055,7 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
952
1055
|
}
|
|
953
1056
|
const now = Date.now();
|
|
954
1057
|
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
1058
|
+
delete metadata.lease;
|
|
955
1059
|
const reviewPassCount = Number(metadata.agent_review_pass_count || 0) + 1;
|
|
956
1060
|
metadata.approval_status = 'pending';
|
|
957
1061
|
metadata.agent_review_pass_count = reviewPassCount;
|
|
@@ -993,9 +1097,9 @@ function readyTask(db, { id, actor, proof, lesson, nextTask, landing = {}, resul
|
|
|
993
1097
|
updated_at = ?,
|
|
994
1098
|
metadata = ?
|
|
995
1099
|
WHERE id = ?
|
|
996
|
-
AND status
|
|
997
|
-
`).run(now, JSON.stringify(metadata), id));
|
|
998
|
-
if (result.changes !== 1) return { ready: false, reason: '
|
|
1100
|
+
AND status = ? AND claimed_by IS ? AND claimed_at IS ?
|
|
1101
|
+
`).run(now, JSON.stringify(metadata), id, row.status, row.claimed_by, row.claimed_at));
|
|
1102
|
+
if (result.changes !== 1) return { ready: false, reason: 'claim changed hands' };
|
|
999
1103
|
const updated = getTask(db, id);
|
|
1000
1104
|
const payload = {
|
|
1001
1105
|
proof: text,
|
|
@@ -2364,6 +2468,12 @@ module.exports = {
|
|
|
2364
2468
|
listTasks,
|
|
2365
2469
|
listTasksCached,
|
|
2366
2470
|
claimTask,
|
|
2471
|
+
setLease,
|
|
2472
|
+
renewLease,
|
|
2473
|
+
clearLease,
|
|
2474
|
+
expireLeases,
|
|
2475
|
+
previewExpiredLeases,
|
|
2476
|
+
sweepExpiredLeases,
|
|
2367
2477
|
releaseTask,
|
|
2368
2478
|
reopenTask,
|
|
2369
2479
|
backlogTask,
|
package/lib/team-lineup.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const { modelLabel } = require('./roster-models');
|
|
8
8
|
const { rosterToolLabel } = require('./engine-registry');
|
|
9
|
+
const { bootRatingText } = require('./atris-rating');
|
|
9
10
|
const { suggestionCount } = require('./roster-suggest');
|
|
10
11
|
|
|
11
12
|
const BUILT_IN_JOBS = Object.freeze(['build', 'review', 'search']);
|
|
@@ -52,6 +53,8 @@ function readLineupSafe(root = process.cwd(), now = new Date()) {
|
|
|
52
53
|
|
|
53
54
|
// One boot line: the tool and model that runs each built-in job right now,
|
|
54
55
|
// custom jobs only when all of them still fit. '' when nothing resolves.
|
|
56
|
+
// A build job's model shows its combine rating (atris rating), since the
|
|
57
|
+
// combine grades building only.
|
|
55
58
|
// When the run record suggests a new order for any job, a short marker
|
|
56
59
|
// points at the roster view; only the end of the record is read.
|
|
57
60
|
function bootTeamLine(root = process.cwd(), { width = 69, now = new Date(), jobs = null } = {}) {
|
|
@@ -63,7 +66,9 @@ function bootTeamLine(root = process.cwd(), { width = 69, now = new Date(), jobs
|
|
|
63
66
|
const ordered = orderJobs(rows);
|
|
64
67
|
const part = (row) => {
|
|
65
68
|
const text = shortRuns(row.now_runs);
|
|
66
|
-
|
|
69
|
+
if (!text) return '';
|
|
70
|
+
const rated = row.role === 'executor' ? bootRatingText(row.now_runs.model) : '';
|
|
71
|
+
return `${row.job} ${text}${rated}`;
|
|
67
72
|
};
|
|
68
73
|
const builtIn = ordered.filter((row) => BUILT_IN_JOBS.includes(row.job) && !row.key).map(part).filter(Boolean);
|
|
69
74
|
const custom = ordered.filter((row) => !BUILT_IN_JOBS.includes(row.job) || row.key).map(part).filter(Boolean);
|
package/package.json
CHANGED