atris 3.63.0 → 3.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/atris/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +28 -2
- package/cli/atris_code.py +25 -1
- package/commands/agent-spawn.js +1 -0
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +3 -3
- package/commands/deck.js +2 -2
- package/commands/engine.js +292 -65
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/hire.js +189 -0
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +47 -9
- package/commands/mission.js +4 -3
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/stream.js +22 -1
- package/commands/task.js +84 -2
- package/commands/team.js +355 -67
- package/commands/transactions.js +3 -3
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +2 -1
- package/lib/auto-accept-certified.js +83 -9
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/business-target.js +131 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +17 -2
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +101 -21
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-identity.js +56 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-models.js +11 -0
- package/lib/roster-runs.js +13 -2
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +22 -6
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +61 -15
- package/scripts/det/ytrail-bench.js +2 -1
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/commands/engine.js
CHANGED
|
@@ -35,9 +35,12 @@ const {
|
|
|
35
35
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
36
36
|
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
37
37
|
const { teamRosterView } = require('../lib/member-engine');
|
|
38
|
-
const { engineRunsView, availableModels } = require('../lib/roster-models');
|
|
38
|
+
const { engineModelText, engineRunsView, availableModels, modelLabel } = require('../lib/roster-models');
|
|
39
|
+
const { renderTable } = require('../lib/text-table');
|
|
40
|
+
const { TEAM_FOOTER } = require('../lib/team-lineup');
|
|
39
41
|
const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
|
|
40
42
|
const { attachSuggestions } = require('../lib/roster-suggest');
|
|
43
|
+
const { PIN_MAX_AGE_DAYS: PIN_STALE_DAYS, attachStaleModels, modelCatalog } = require('../lib/roster-stale');
|
|
41
44
|
const {
|
|
42
45
|
ENGINE_ROLES,
|
|
43
46
|
ENGINE_JOBS,
|
|
@@ -69,6 +72,8 @@ const {
|
|
|
69
72
|
readEngineRegistry,
|
|
70
73
|
resolveEngineForRole,
|
|
71
74
|
resolveEngineForRoleRanked,
|
|
75
|
+
resolveNotesJob,
|
|
76
|
+
jobTakesAnyEngine,
|
|
72
77
|
requireEngineBin,
|
|
73
78
|
engineDoctorReport,
|
|
74
79
|
engineFailureHealthStatus,
|
|
@@ -1000,10 +1005,41 @@ function rosterPickEntry(value) {
|
|
|
1000
1005
|
|
|
1001
1006
|
const WORKER_SKIP_WORDS = Object.freeze({ 'not ready': 'down', expired: 'expired', 'bad model': 'names a model it cannot run' });
|
|
1002
1007
|
|
|
1008
|
+
// A worker whose end date is this many days away or fewer gets a heads-up.
|
|
1009
|
+
const ENDS_SOON_DAYS = 7;
|
|
1010
|
+
const RENEW_COMMAND = 'atris engine roster confirm';
|
|
1011
|
+
|
|
1012
|
+
// Whole local days from today to a worker's end date: 0 on its last day,
|
|
1013
|
+
// negative once it has ended, null when it has no valid end date.
|
|
1014
|
+
function endsInDays(until, now = new Date()) {
|
|
1015
|
+
const end = parseRosterUntil(until);
|
|
1016
|
+
if (!end) return null;
|
|
1017
|
+
const date = new Date(now);
|
|
1018
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
1019
|
+
const today = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1020
|
+
return Math.round((end.getTime() - today.getTime()) / 86400000);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function endsSoon(days) {
|
|
1024
|
+
return days !== null && days >= 0 && days <= ENDS_SOON_DAYS;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function endsInText(days) {
|
|
1028
|
+
if (days === 0) return 'ends today';
|
|
1029
|
+
if (days === 1) return 'ends tomorrow';
|
|
1030
|
+
return `ends in ${days} days`;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function shortDay(until) {
|
|
1034
|
+
const date = parseRosterUntil(until);
|
|
1035
|
+
return date ? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toLowerCase() : '';
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1003
1038
|
// One worker of a job's team as the view shows it: what it runs, and whether
|
|
1004
1039
|
// it leads now, backs up, or is skipped and why.
|
|
1005
|
-
function workerRow(step, index, leadIndex) {
|
|
1040
|
+
function workerRow(step, index, leadIndex, now = new Date()) {
|
|
1006
1041
|
const { worker } = step;
|
|
1042
|
+
const endsIn = endsInDays(worker.until, now);
|
|
1007
1043
|
const runs = worker.error ? null : engineRunsView(worker.engine, { model: worker.model || '', effort: worker.effort || '' });
|
|
1008
1044
|
const status = index === leadIndex ? 'leads' : step.skip ? 'skipped' : 'backup';
|
|
1009
1045
|
const why = step.skip === 'bad line' ? `bad line: ${worker.error}`
|
|
@@ -1016,6 +1052,9 @@ function workerRow(step, index, leadIndex) {
|
|
|
1016
1052
|
max_seconds: Number(worker.max_seconds) > 0 ? Number(worker.max_seconds) : null,
|
|
1017
1053
|
prep: worker.prep ? rosterJobLabel(worker.prep) : null,
|
|
1018
1054
|
until: worker.until || null,
|
|
1055
|
+
ends_in_days: endsIn,
|
|
1056
|
+
expiring_soon: endsSoon(endsIn),
|
|
1057
|
+
verified: worker.verified || null,
|
|
1019
1058
|
line: worker.line || null,
|
|
1020
1059
|
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1021
1060
|
status,
|
|
@@ -1041,7 +1080,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1041
1080
|
const decided = rosterDecided(resolved.source) && (!custom || resolved.job === job);
|
|
1042
1081
|
const source = decided ? resolved.source : ROSTER_LAYERS.find((scope) => picks[scope]) || null;
|
|
1043
1082
|
const pick = source ? picks[source] : null;
|
|
1044
|
-
const walk = pick ? rosterWorkerWalk(pick, role, registry, now) : [];
|
|
1083
|
+
const walk = pick ? rosterWorkerWalk(pick, role, registry, now, { anyEngine: jobTakesAnyEngine(key) }) : [];
|
|
1045
1084
|
const leadIndex = decided && resolved.source === source ? walk.findIndex((step) => !step.skip) : -1;
|
|
1046
1085
|
const good = walk.map((step, index) => ({ step, index })).filter(({ step }) => !step.worker.error);
|
|
1047
1086
|
const first = good[0] || null;
|
|
@@ -1055,6 +1094,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1055
1094
|
: good[1] && leadIndex === good[1].index ? 'backup'
|
|
1056
1095
|
: 'later';
|
|
1057
1096
|
const kind = Object.keys(ENGINE_JOBS).find((name) => ENGINE_JOBS[name] === role);
|
|
1097
|
+
const pickEndsIn = pick ? endsInDays(pick.until, now) : null;
|
|
1058
1098
|
// What the job really runs: its first two good workers, each with its own
|
|
1059
1099
|
// model and effort, or the router's engine when no line decides.
|
|
1060
1100
|
const runsOf = (entry) => (entry ? engineRunsView(entry.step.worker.engine, { model: entry.step.worker.model || '', effort: entry.step.worker.effort || '' }) : null);
|
|
@@ -1076,12 +1116,14 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1076
1116
|
model: resolved.engine && resolved.engine.roster_model ? resolved.engine.roster_model : null,
|
|
1077
1117
|
effort: resolved.engine && resolved.engine.roster_effort ? resolved.engine.roster_effort : null,
|
|
1078
1118
|
max_seconds: pick && Number(pick.max_seconds) > 0 ? Number(pick.max_seconds) : null,
|
|
1119
|
+
ends_in_days: pickEndsIn,
|
|
1120
|
+
expiring_soon: endsSoon(pickEndsIn),
|
|
1079
1121
|
prep: first && first.step.worker.prep ? rosterJobLabel(first.step.worker.prep) : null,
|
|
1080
1122
|
backup_prep: good[1] && good[1].step.worker.prep ? rosterJobLabel(good[1].step.worker.prep) : null,
|
|
1081
1123
|
runs: runs || nowRuns,
|
|
1082
1124
|
backup_runs: runsOf(good[1]),
|
|
1083
1125
|
now_runs: nowRuns,
|
|
1084
|
-
workers: walk.map((step, index) => workerRow(step, index, leadIndex)),
|
|
1126
|
+
workers: walk.map((step, index) => workerRow(step, index, leadIndex, now)),
|
|
1085
1127
|
lead,
|
|
1086
1128
|
status,
|
|
1087
1129
|
...(status === 'cooling' ? { cooling: first.step.cooling.text } : {}),
|
|
@@ -1114,8 +1156,11 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1114
1156
|
const state = readRosterState(root, { now });
|
|
1115
1157
|
const jobs = jobRosterView(root, now, state);
|
|
1116
1158
|
const team = teamRosterView(root, { now, rosterState: state });
|
|
1159
|
+
const expiring = expiringWorkers(jobs);
|
|
1117
1160
|
return {
|
|
1118
1161
|
jobs,
|
|
1162
|
+
expiring,
|
|
1163
|
+
renew_command: expiring.length ? RENEW_COMMAND : null,
|
|
1119
1164
|
team: team.rows,
|
|
1120
1165
|
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1121
1166
|
files: {
|
|
@@ -1127,75 +1172,203 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1127
1172
|
};
|
|
1128
1173
|
}
|
|
1129
1174
|
|
|
1130
|
-
|
|
1175
|
+
// Every worker, in any job, whose end date is today or within the next
|
|
1176
|
+
// seven days. Workers already past their date show on their own lines. A
|
|
1177
|
+
// job set for this shell only is skipped: roster confirm does not renew it.
|
|
1178
|
+
function expiringWorkers(jobs) {
|
|
1179
|
+
const list = [];
|
|
1180
|
+
for (const row of jobs || []) {
|
|
1181
|
+
if (row.session_pick) continue;
|
|
1182
|
+
for (const worker of row.workers || []) {
|
|
1183
|
+
if (!worker.expiring_soon) continue;
|
|
1184
|
+
list.push({
|
|
1185
|
+
job: row.job,
|
|
1186
|
+
engine: worker.engine,
|
|
1187
|
+
model: worker.model,
|
|
1188
|
+
tool: worker.runs ? worker.runs.text : worker.engine,
|
|
1189
|
+
until: worker.until,
|
|
1190
|
+
ends_in_days: worker.ends_in_days,
|
|
1191
|
+
from: row.from,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return list;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function untilText(worker, days = null) {
|
|
1131
1199
|
const until = parseRosterUntil(worker.until);
|
|
1132
|
-
if (until) return `until ${until
|
|
1200
|
+
if (until) return `until ${shortDay(worker.until)}${endsSoon(days) ? `, ${endsInText(days)}` : ''}`;
|
|
1133
1201
|
return worker.never_expires ? 'no end date' : 'until no valid date';
|
|
1134
1202
|
}
|
|
1135
1203
|
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
function
|
|
1139
|
-
if (!
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.prep ? `prepped by ${worker.prep}` : '', worker.until ? `until ${worker.until}` : ''].filter(Boolean);
|
|
1144
|
-
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup' : `skipped, ${worker.why}`;
|
|
1145
|
-
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1146
|
-
});
|
|
1204
|
+
// One line under the jobs when any worker ends within seven days, naming
|
|
1205
|
+
// each one and the command that keeps it going.
|
|
1206
|
+
function renderExpiringLine(expiring) {
|
|
1207
|
+
if (!expiring || !expiring.length) return '';
|
|
1208
|
+
const names = expiring.map((item) => `${item.tool} on ${item.job} ${endsInText(item.ends_in_days)}`);
|
|
1209
|
+
const them = expiring.length === 1 ? 'it' : 'them';
|
|
1210
|
+
return `heads up: ${names.join(', ')}. to keep ${them} 30 more days, run: ${RENEW_COMMAND}`;
|
|
1147
1211
|
}
|
|
1148
1212
|
|
|
1149
|
-
//
|
|
1150
|
-
function
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1213
|
+
// The same heads-up, short enough for one boot line.
|
|
1214
|
+
function bootExpiringLine(expiring) {
|
|
1215
|
+
if (!expiring || !expiring.length) return '';
|
|
1216
|
+
const [first] = expiring;
|
|
1217
|
+
const more = expiring.length > 1 ? ` and ${expiring.length - 1} more` : '';
|
|
1218
|
+
return `${first.tool} on ${first.job} ${endsInText(first.ends_in_days)}${more}. renew: ${RENEW_COMMAND}`;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// --- the roster as tables -------------------------------------------------
|
|
1222
|
+
//
|
|
1223
|
+
// One row per job, one row per member. Plain columns, a header row, two
|
|
1224
|
+
// spaces between; a cell that needs more than one line (effort, several
|
|
1225
|
+
// backups) carries on underneath in its own column.
|
|
1226
|
+
|
|
1227
|
+
function workerCellText(view) {
|
|
1228
|
+
return view ? engineModelText(view.engine, view.model) : '';
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function maxCellText(seconds) {
|
|
1232
|
+
const text = rosterMaxText(seconds);
|
|
1233
|
+
return text ? text.replace(/^max /, '') : '-';
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Workers that name a tool the roster can run, in written order.
|
|
1237
|
+
function goodWorkers(row) {
|
|
1238
|
+
return (row.workers || []).filter((worker) => worker.engine && worker.runs);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Why a backup is passed over, in two or three words.
|
|
1242
|
+
function backupSkipText(worker) {
|
|
1243
|
+
if (worker.status !== 'skipped') return '';
|
|
1244
|
+
if (worker.why === 'expired' && worker.ends_in_days !== null && worker.ends_in_days !== undefined) return `ended ${shortDay(worker.until)}`;
|
|
1245
|
+
return worker.why || 'skipped';
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// "newer <model> out" or "pin 45 days old": the outdated-model flags,
|
|
1249
|
+
// short enough for the note column. The full line with its fix prints
|
|
1250
|
+
// under the table.
|
|
1251
|
+
function staleNote(worker) {
|
|
1252
|
+
if (worker.stale_model && worker.newer_model) return `newer ${modelLabel(worker.newer_model)} out`;
|
|
1253
|
+
if (worker.pin_age_days !== null && worker.pin_age_days !== undefined && worker.pin_age_days > PIN_STALE_DAYS) return `pin ${worker.pin_age_days} days old`;
|
|
1254
|
+
return '';
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function jobNoteText(row, lead, backups, mainFrom) {
|
|
1258
|
+
// A tool on its own default model is named alone here, to keep notes short.
|
|
1259
|
+
const nowText = !row.now_runs ? 'no ready engine' : row.now_runs.model ? workerCellText(row.now_runs) : row.now_runs.engine;
|
|
1260
|
+
if (!row.pick) return row.like ? `no pick, same as ${row.like}` : 'no pick, router decides';
|
|
1261
|
+
const fallback = row.lead === 'backup' ? 'using backup'
|
|
1262
|
+
: row.lead === 'later' ? `using ${nowText}`
|
|
1263
|
+
: row.like ? `falls back to ${row.like}` : `router decides: ${nowText}`;
|
|
1264
|
+
const ended = row.ends_in_days !== null && row.ends_in_days !== undefined ? `ended ${shortDay(row.pick.until)}` : 'expired';
|
|
1265
|
+
const parts = [];
|
|
1266
|
+
if (row.status === 'expired') parts.push(`${ended}, ${fallback}`);
|
|
1267
|
+
else if (row.status === 'cooling') parts.push(`${row.cooling}, ${fallback}`);
|
|
1268
|
+
else if (row.status === 'not ready') parts.push(`not ready, ${fallback}`);
|
|
1269
|
+
else if (row.expiring_soon) parts.push(endsInText(row.ends_in_days));
|
|
1270
|
+
if (lead && lead.prep) parts.push(`prep: ${lead.prep}`);
|
|
1271
|
+
if (lead && staleNote(lead)) parts.push(staleNote(lead));
|
|
1272
|
+
for (const worker of backups) if (staleNote(worker)) parts.push(`backup ${staleNote(worker)}`);
|
|
1273
|
+
if (row.from && row.from !== mainFrom) parts.push(`from ${row.from}`);
|
|
1274
|
+
return parts.join(', ');
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// The roster file most jobs come from; the table heading names it.
|
|
1278
|
+
function mainRosterSource(rows) {
|
|
1279
|
+
const counts = new Map();
|
|
1280
|
+
for (const row of rows) if (row.pick && row.from) counts.set(row.from, (counts.get(row.from) || 0) + 1);
|
|
1281
|
+
const [from] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] || [];
|
|
1282
|
+
const row = rows.find((item) => item.from === from);
|
|
1283
|
+
return { from: from || null, file: row ? row.file : null };
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function jobTableRow(row, mainFrom) {
|
|
1287
|
+
const [lead, ...backups] = row.pick ? goodWorkers(row) : [];
|
|
1288
|
+
const leadView = row.pick ? (lead && lead.runs) : row.now_runs;
|
|
1289
|
+
const leads = leadView
|
|
1290
|
+
? [workerCellText(leadView), leadView.effort ? `(${leadView.effort} effort)` : null]
|
|
1291
|
+
: [row.pick ? 'no ready tool' : 'no ready engine'];
|
|
1292
|
+
const backupCell = backups.length
|
|
1293
|
+
? backups.map((worker) => {
|
|
1294
|
+
// Effort, max time, and prep for a backup stay in --json; the cell keeps
|
|
1295
|
+
// what changes who runs: a skip reason or an end date.
|
|
1296
|
+
const skip = backupSkipText(worker);
|
|
1297
|
+
const until = !skip && parseRosterUntil(worker.until) ? `until ${shortDay(worker.until)}${worker.expiring_soon ? `, ${endsInText(worker.ends_in_days)}` : ''}` : '';
|
|
1298
|
+
const extras = [skip, until].filter(Boolean);
|
|
1299
|
+
return `${workerCellText(worker.runs)}${extras.length ? ` (${extras.join(', ')})` : ''}`;
|
|
1300
|
+
})
|
|
1301
|
+
: ['none'];
|
|
1302
|
+
const until = lead && parseRosterUntil(lead.until) ? lead.until : '-';
|
|
1303
|
+
return [
|
|
1304
|
+
row.job,
|
|
1305
|
+
leads,
|
|
1306
|
+
backupCell,
|
|
1307
|
+
lead ? maxCellText(lead.max_seconds) : '-',
|
|
1308
|
+
until,
|
|
1309
|
+
jobNoteText(row, lead, backups, mainFrom) || '-',
|
|
1310
|
+
];
|
|
1179
1311
|
}
|
|
1180
1312
|
|
|
1181
|
-
function
|
|
1313
|
+
function renderJobRoster(rows, { width, heading = true } = {}) {
|
|
1182
1314
|
if (!rows.length) return '';
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1315
|
+
const { from, file } = mainRosterSource(rows);
|
|
1316
|
+
const columns = [
|
|
1317
|
+
{ header: 'JOB' }, { header: 'LEADS' }, { header: 'BACKUP' }, { header: 'MAX' }, { header: 'UNTIL' },
|
|
1318
|
+
{ header: 'NOTE', wrap: true, min: 16 },
|
|
1319
|
+
];
|
|
1320
|
+
const table = renderTable(columns, rows.map((row) => jobTableRow(row, from)), { width });
|
|
1321
|
+
const title = !heading ? '' : file ? `jobs, from ${file}` : 'jobs';
|
|
1322
|
+
const suggestions = rows.filter((row) => row.suggestion).map((row) => `suggestion for ${row.job}: ${row.suggestion.text}`);
|
|
1323
|
+
return [title, table, ...renderWorkerRecords(rows, { width }), ...suggestions].filter(Boolean).join('\n');
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Each worker's recent record (last 7 days), only for workers with runs, as
|
|
1327
|
+
// one small table under the jobs.
|
|
1328
|
+
function renderWorkerRecords(rows, { width } = {}) {
|
|
1329
|
+
const records = [];
|
|
1330
|
+
for (const row of rows) {
|
|
1331
|
+
for (const worker of row.workers || []) {
|
|
1332
|
+
if (worker.record) records.push([row.job, worker.runs ? workerCellText(worker.runs) : worker.engine, worker.record.text.replace(/^last \d+ days: /, '')]);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (!records.length) return [];
|
|
1336
|
+
return ['\nlast 7 days', renderTable([{ header: 'JOB' }, { header: 'WORKER' }, { header: 'RECORD', clip: true, min: 20 }], records, { width })];
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// "sep 30, 3 runs" or "quiet 8 to 14 days", from the same evidence atris
|
|
1340
|
+
// team reads. '-' for idle members and when activity was not read.
|
|
1341
|
+
function teamActivityTag(activity) {
|
|
1342
|
+
if (!activity || activity.status === 'idle') return '-';
|
|
1343
|
+
if (activity.status === 'quiet') return 'quiet 8 to 14 days';
|
|
1344
|
+
const runs = Number(activity.runs_7d) || 0;
|
|
1345
|
+
const day = activity.days_since === 0 ? 'today' : activity.days_since === 1 ? 'yesterday' : shortDay(activity.last_active);
|
|
1346
|
+
return `${day}, ${runs} run${runs === 1 ? '' : 's'}`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function renderTeamRoster(rows, { width } = {}) {
|
|
1350
|
+
if (!rows.length) return '';
|
|
1351
|
+
const showActivity = rows.some((row) => row.activity !== undefined);
|
|
1352
|
+
const columns = [{ header: 'MEMBER' }, { header: 'JOB' }, { header: 'ENGINE · MODEL' }, { header: 'SOURCE' }];
|
|
1353
|
+
if (showActivity) columns.push({ header: 'LAST 7 DAYS', clip: true, min: 11 });
|
|
1354
|
+
const cells = rows.map((row) => {
|
|
1355
|
+
const engine = row.engine ? workerCellText(engineRunsView(row.engine, { model: row.model || '', effort: row.effort || '' })) : 'no ready engine';
|
|
1356
|
+
const stale = row.stale_model && row.newer_model ? ` (newer ${modelLabel(row.newer_model)} out)` : '';
|
|
1357
|
+
const cell = [row.member, row.job || '-', `${engine}${stale}`, row.source === 'file' ? (row.file || 'roster file') : 'automatic'];
|
|
1358
|
+
if (showActivity) cell.push(teamActivityTag(row.activity));
|
|
1359
|
+
return cell;
|
|
1189
1360
|
});
|
|
1190
|
-
return ['team',
|
|
1361
|
+
return ['team', renderTable(columns, cells, { width })].join('\n');
|
|
1191
1362
|
}
|
|
1192
1363
|
|
|
1193
1364
|
function renderRosterWarnings(warnings) {
|
|
1194
1365
|
return warnings.map((warning) => `warning: ${warning.file} line ${warning.line} "${warning.text}" ${warning.message}.`).join('\n');
|
|
1195
1366
|
}
|
|
1196
1367
|
|
|
1197
|
-
function renderRosterReport(report) {
|
|
1198
|
-
|
|
1368
|
+
function renderRosterReport(report, { width } = {}) {
|
|
1369
|
+
const stale = (report.stale || []).map((item) => item.text);
|
|
1370
|
+
const jobs = [renderJobRoster(report.jobs, { width }), renderExpiringLine(report.expiring), ...stale].filter(Boolean).join('\n');
|
|
1371
|
+
return [jobs, renderTeamRoster(report.team, { width }), renderRosterWarnings(report.warnings)]
|
|
1199
1372
|
.filter(Boolean)
|
|
1200
1373
|
.join('\n\n');
|
|
1201
1374
|
}
|
|
@@ -1204,8 +1377,6 @@ function printJobRoster(root, now = new Date()) {
|
|
|
1204
1377
|
console.log(renderRosterReport(rosterReport(root, now)));
|
|
1205
1378
|
}
|
|
1206
1379
|
|
|
1207
|
-
const AVAILABLE_HINT = 'see which tools and models this machine has: atris engine roster --available';
|
|
1208
|
-
|
|
1209
1380
|
// Each installed tool and the models it offers, from local files only.
|
|
1210
1381
|
function availableReport(root) {
|
|
1211
1382
|
const engines = readEngineRegistry(root, { persist: false }).engines
|
|
@@ -1226,7 +1397,7 @@ function renderSessionRoster(report, key) {
|
|
|
1226
1397
|
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1227
1398
|
if (!jobs.length && !team.length) return `this session (${key}) has no roster changes. add one with: atris engine assign <job> <tool> --session`;
|
|
1228
1399
|
const head = `this session (${key}) changes${report.files.session ? `, from ${report.files.session}` : ''}`;
|
|
1229
|
-
return [head, renderJobRoster(jobs), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1400
|
+
return [head, renderJobRoster(jobs, { heading: false }), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1230
1401
|
}
|
|
1231
1402
|
|
|
1232
1403
|
function runRosterSessionCommand(rest, json, root, now) {
|
|
@@ -1275,6 +1446,38 @@ function attachRunRecords(report, root, now = new Date()) {
|
|
|
1275
1446
|
return report;
|
|
1276
1447
|
}
|
|
1277
1448
|
|
|
1449
|
+
// Pins are proven by landed runs; a pin older than 30 days is flagged, so
|
|
1450
|
+
// the look-back reaches well past that.
|
|
1451
|
+
const STALE_PROOF_DAYS = 90;
|
|
1452
|
+
|
|
1453
|
+
// Each team row gets the member's activity (last 7 days, quiet, or idle)
|
|
1454
|
+
// from the same evidence atris team reads: dated logs, commits that name
|
|
1455
|
+
// the member, roster runs, and stream events, read once for the team.
|
|
1456
|
+
function attachTeamActivity(report, root, now = new Date()) {
|
|
1457
|
+
const rows = report.team || [];
|
|
1458
|
+
if (!rows.length) return report;
|
|
1459
|
+
try {
|
|
1460
|
+
const { collectMemberActivity } = require('./team');
|
|
1461
|
+
const activity = collectMemberActivity(root, rows.map((row) => ({ name: row.member })), { nowMs: new Date(now).getTime() });
|
|
1462
|
+
const byName = new Map(activity.map(({ days, ...rest }) => [rest.name, rest]));
|
|
1463
|
+
for (const row of rows) row.activity = byName.get(String(row.member).toLowerCase()) || null;
|
|
1464
|
+
} catch {
|
|
1465
|
+
// Activity is a view; a read that fails leaves the roster as it was.
|
|
1466
|
+
}
|
|
1467
|
+
return report;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Mark workers that run an outdated model or an unproven old pin. Local
|
|
1471
|
+
// files only: the registry, codex's models cache, and the run record.
|
|
1472
|
+
function attachStaleCheck(report, root, now = new Date()) {
|
|
1473
|
+
const engines = readEngineRegistry(root, { persist: false }).engines;
|
|
1474
|
+
return attachStaleModels(report, {
|
|
1475
|
+
catalog: modelCatalog(engines),
|
|
1476
|
+
runs: readRosterRuns(root, { now, days: STALE_PROOF_DAYS }),
|
|
1477
|
+
now,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1278
1481
|
const RUNS_LIST_DAYS = 30;
|
|
1279
1482
|
const RUNS_LIST_LIMIT = 20;
|
|
1280
1483
|
|
|
@@ -1321,9 +1524,9 @@ function runRosterCommand(args, root, now = new Date()) {
|
|
|
1321
1524
|
}
|
|
1322
1525
|
if (rest[0] === 'session') return runRosterSessionCommand(rest, json, root, now);
|
|
1323
1526
|
if (rest[0] === 'confirm') confirmRoster(root, now);
|
|
1324
|
-
const report = attachRunRecords(rosterReport(root, now), root, now);
|
|
1527
|
+
const report = attachTeamActivity(attachStaleCheck(attachRunRecords(rosterReport(root, now), root, now), root, now), root, now);
|
|
1325
1528
|
if (json) console.log(JSON.stringify(report, null, 2));
|
|
1326
|
-
else console.log(`${renderRosterReport(report)}\n\n${
|
|
1529
|
+
else console.log(`${renderRosterReport(report)}\n\n${TEAM_FOOTER.join('\n')}`);
|
|
1327
1530
|
return 0;
|
|
1328
1531
|
}
|
|
1329
1532
|
|
|
@@ -1530,9 +1733,30 @@ function parseSetFlag(args) {
|
|
|
1530
1733
|
return '';
|
|
1531
1734
|
}
|
|
1532
1735
|
|
|
1533
|
-
|
|
1736
|
+
// One worker as a person reads it: the tool, then its model and effort.
|
|
1737
|
+
function notesWorkerText(worker) {
|
|
1738
|
+
const details = [worker.model, worker.effort ? `effort ${worker.effort}` : '', worker.installed ? '' : 'not installed'].filter(Boolean);
|
|
1739
|
+
return details.length ? `${worker.engine} (${details.join(', ')})` : worker.engine;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// The notes job: the roster's pick when one is set, else the built-in
|
|
1743
|
+
// default. Always answers, so scripts can call it with a short timeout.
|
|
1744
|
+
function runNotesResolve(json, root, now) {
|
|
1745
|
+
const answer = resolveNotesJob(root, { now });
|
|
1746
|
+
if (json) {
|
|
1747
|
+
console.log(JSON.stringify(answer));
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
1750
|
+
const backup = answer.backup.length ? `backup ${answer.backup.map(notesWorkerText).join(', then ')}` : 'no backup';
|
|
1751
|
+
const where = answer.source === 'roster' ? `from ${answer.from}` : `built-in default${answer.skipped ? ` (${answer.skipped})` : ''}`;
|
|
1752
|
+
console.log(`notes: ${notesWorkerText(answer)}, ${backup}, ${where}`);
|
|
1753
|
+
return 0;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function runResolveCommand(args, root, now) {
|
|
1534
1757
|
const json = args.includes('--json');
|
|
1535
1758
|
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
1759
|
+
if (role && rosterJobKey(role) === 'notes') return runNotesResolve(json, root, now);
|
|
1536
1760
|
if (!role) {
|
|
1537
1761
|
const message = `usage: atris engine resolve <role> or <job>; roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or your own`;
|
|
1538
1762
|
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
@@ -2133,7 +2357,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2133
2357
|
}
|
|
2134
2358
|
|
|
2135
2359
|
if (sub === 'resolve') {
|
|
2136
|
-
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root);
|
|
2360
|
+
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root, deps.now);
|
|
2137
2361
|
}
|
|
2138
2362
|
|
|
2139
2363
|
if (sub === 'health') {
|
|
@@ -2160,7 +2384,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2160
2384
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
2161
2385
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
2162
2386
|
console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
|
|
2163
|
-
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2387
|
+
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n flags a model with a newer version on this machine, or a pin not proven in 30 days\n (a landed run, or "verified 2026-09-30" on its line, proves it)\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2164
2388
|
return 0;
|
|
2165
2389
|
}
|
|
2166
2390
|
if (isFreshWorkspace(root)) {
|
|
@@ -2209,7 +2433,10 @@ module.exports = {
|
|
|
2209
2433
|
readSavedEngine,
|
|
2210
2434
|
roster,
|
|
2211
2435
|
rosterReport,
|
|
2436
|
+
renderRosterReport,
|
|
2212
2437
|
jobRosterView,
|
|
2438
|
+
expiringWorkers,
|
|
2439
|
+
bootExpiringLine,
|
|
2213
2440
|
engineRegistryFile,
|
|
2214
2441
|
readEngineRegistry,
|
|
2215
2442
|
resolveEngineForRole,
|
package/commands/feedback.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const { stateHome } = require('../lib/state-home');
|
|
20
21
|
const { loadCredentials } = require('../utils/auth');
|
|
21
22
|
const { apiRequestJson } = require('../utils/api');
|
|
22
23
|
const {
|
|
@@ -297,8 +298,7 @@ function resolveBusinessArg(value) {
|
|
|
297
298
|
// Full UUID, trust it
|
|
298
299
|
if (/^[0-9a-f-]{32,}$/i.test(value)) return value;
|
|
299
300
|
// Otherwise treat as slug and look up in ~/.atris/businesses.json
|
|
300
|
-
const
|
|
301
|
-
const file = path.join(home, '.atris', 'businesses.json');
|
|
301
|
+
const file = path.join(stateHome(), '.atris', 'businesses.json');
|
|
302
302
|
if (!fs.existsSync(file)) return null;
|
|
303
303
|
try {
|
|
304
304
|
const map = JSON.parse(fs.readFileSync(file, 'utf8'));
|
package/commands/fork.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
|
|
@@ -48,7 +48,7 @@ async function forkAtris() {
|
|
|
48
48
|
files = downloadResult.data.files;
|
|
49
49
|
} else {
|
|
50
50
|
// Fall back to local template at ~/.atris/templates/{template}/
|
|
51
|
-
const localTemplatePath = path.join(
|
|
51
|
+
const localTemplatePath = path.join(stateHome(), '.atris', 'templates', template);
|
|
52
52
|
if (fs.existsSync(localTemplatePath)) {
|
|
53
53
|
console.log(' API unavailable, using local template...');
|
|
54
54
|
files = readLocalTemplate(localTemplatePath, localTemplatePath);
|
package/commands/gm.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
7
|
const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
|
|
7
8
|
|
|
8
9
|
const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
|
|
@@ -452,7 +453,7 @@ function feedPostId(post) {
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
function credentialsFileExists() {
|
|
455
|
-
return fs.existsSync(path.join(
|
|
456
|
+
return fs.existsSync(path.join(stateHome(), '.atris', 'credentials.json'));
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
async function fetchFeedEvents(workspaceRoot) {
|