atris 3.63.0 → 3.64.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/atris/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +11 -2
- package/cli/atris_code.py +25 -1
- 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 +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +47 -9
- package/commands/mission.js +1 -1
- 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/task.js +84 -2
- 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/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -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-registry.js +85 -18
- 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/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- 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/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
|
@@ -69,6 +69,8 @@ const {
|
|
|
69
69
|
readEngineRegistry,
|
|
70
70
|
resolveEngineForRole,
|
|
71
71
|
resolveEngineForRoleRanked,
|
|
72
|
+
resolveNotesJob,
|
|
73
|
+
jobTakesAnyEngine,
|
|
72
74
|
requireEngineBin,
|
|
73
75
|
engineDoctorReport,
|
|
74
76
|
engineFailureHealthStatus,
|
|
@@ -1000,10 +1002,41 @@ function rosterPickEntry(value) {
|
|
|
1000
1002
|
|
|
1001
1003
|
const WORKER_SKIP_WORDS = Object.freeze({ 'not ready': 'down', expired: 'expired', 'bad model': 'names a model it cannot run' });
|
|
1002
1004
|
|
|
1005
|
+
// A worker whose end date is this many days away or fewer gets a heads-up.
|
|
1006
|
+
const ENDS_SOON_DAYS = 7;
|
|
1007
|
+
const RENEW_COMMAND = 'atris engine roster confirm';
|
|
1008
|
+
|
|
1009
|
+
// Whole local days from today to a worker's end date: 0 on its last day,
|
|
1010
|
+
// negative once it has ended, null when it has no valid end date.
|
|
1011
|
+
function endsInDays(until, now = new Date()) {
|
|
1012
|
+
const end = parseRosterUntil(until);
|
|
1013
|
+
if (!end) return null;
|
|
1014
|
+
const date = new Date(now);
|
|
1015
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
1016
|
+
const today = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
1017
|
+
return Math.round((end.getTime() - today.getTime()) / 86400000);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function endsSoon(days) {
|
|
1021
|
+
return days !== null && days >= 0 && days <= ENDS_SOON_DAYS;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function endsInText(days) {
|
|
1025
|
+
if (days === 0) return 'ends today';
|
|
1026
|
+
if (days === 1) return 'ends tomorrow';
|
|
1027
|
+
return `ends in ${days} days`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function shortDay(until) {
|
|
1031
|
+
const date = parseRosterUntil(until);
|
|
1032
|
+
return date ? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toLowerCase() : '';
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1003
1035
|
// One worker of a job's team as the view shows it: what it runs, and whether
|
|
1004
1036
|
// it leads now, backs up, or is skipped and why.
|
|
1005
|
-
function workerRow(step, index, leadIndex) {
|
|
1037
|
+
function workerRow(step, index, leadIndex, now = new Date()) {
|
|
1006
1038
|
const { worker } = step;
|
|
1039
|
+
const endsIn = endsInDays(worker.until, now);
|
|
1007
1040
|
const runs = worker.error ? null : engineRunsView(worker.engine, { model: worker.model || '', effort: worker.effort || '' });
|
|
1008
1041
|
const status = index === leadIndex ? 'leads' : step.skip ? 'skipped' : 'backup';
|
|
1009
1042
|
const why = step.skip === 'bad line' ? `bad line: ${worker.error}`
|
|
@@ -1016,6 +1049,8 @@ function workerRow(step, index, leadIndex) {
|
|
|
1016
1049
|
max_seconds: Number(worker.max_seconds) > 0 ? Number(worker.max_seconds) : null,
|
|
1017
1050
|
prep: worker.prep ? rosterJobLabel(worker.prep) : null,
|
|
1018
1051
|
until: worker.until || null,
|
|
1052
|
+
ends_in_days: endsIn,
|
|
1053
|
+
expiring_soon: endsSoon(endsIn),
|
|
1019
1054
|
line: worker.line || null,
|
|
1020
1055
|
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1021
1056
|
status,
|
|
@@ -1041,7 +1076,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1041
1076
|
const decided = rosterDecided(resolved.source) && (!custom || resolved.job === job);
|
|
1042
1077
|
const source = decided ? resolved.source : ROSTER_LAYERS.find((scope) => picks[scope]) || null;
|
|
1043
1078
|
const pick = source ? picks[source] : null;
|
|
1044
|
-
const walk = pick ? rosterWorkerWalk(pick, role, registry, now) : [];
|
|
1079
|
+
const walk = pick ? rosterWorkerWalk(pick, role, registry, now, { anyEngine: jobTakesAnyEngine(key) }) : [];
|
|
1045
1080
|
const leadIndex = decided && resolved.source === source ? walk.findIndex((step) => !step.skip) : -1;
|
|
1046
1081
|
const good = walk.map((step, index) => ({ step, index })).filter(({ step }) => !step.worker.error);
|
|
1047
1082
|
const first = good[0] || null;
|
|
@@ -1055,6 +1090,7 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1055
1090
|
: good[1] && leadIndex === good[1].index ? 'backup'
|
|
1056
1091
|
: 'later';
|
|
1057
1092
|
const kind = Object.keys(ENGINE_JOBS).find((name) => ENGINE_JOBS[name] === role);
|
|
1093
|
+
const pickEndsIn = pick ? endsInDays(pick.until, now) : null;
|
|
1058
1094
|
// What the job really runs: its first two good workers, each with its own
|
|
1059
1095
|
// model and effort, or the router's engine when no line decides.
|
|
1060
1096
|
const runsOf = (entry) => (entry ? engineRunsView(entry.step.worker.engine, { model: entry.step.worker.model || '', effort: entry.step.worker.effort || '' }) : null);
|
|
@@ -1076,12 +1112,14 @@ function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
|
1076
1112
|
model: resolved.engine && resolved.engine.roster_model ? resolved.engine.roster_model : null,
|
|
1077
1113
|
effort: resolved.engine && resolved.engine.roster_effort ? resolved.engine.roster_effort : null,
|
|
1078
1114
|
max_seconds: pick && Number(pick.max_seconds) > 0 ? Number(pick.max_seconds) : null,
|
|
1115
|
+
ends_in_days: pickEndsIn,
|
|
1116
|
+
expiring_soon: endsSoon(pickEndsIn),
|
|
1079
1117
|
prep: first && first.step.worker.prep ? rosterJobLabel(first.step.worker.prep) : null,
|
|
1080
1118
|
backup_prep: good[1] && good[1].step.worker.prep ? rosterJobLabel(good[1].step.worker.prep) : null,
|
|
1081
1119
|
runs: runs || nowRuns,
|
|
1082
1120
|
backup_runs: runsOf(good[1]),
|
|
1083
1121
|
now_runs: nowRuns,
|
|
1084
|
-
workers: walk.map((step, index) => workerRow(step, index, leadIndex)),
|
|
1122
|
+
workers: walk.map((step, index) => workerRow(step, index, leadIndex, now)),
|
|
1085
1123
|
lead,
|
|
1086
1124
|
status,
|
|
1087
1125
|
...(status === 'cooling' ? { cooling: first.step.cooling.text } : {}),
|
|
@@ -1114,8 +1152,11 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1114
1152
|
const state = readRosterState(root, { now });
|
|
1115
1153
|
const jobs = jobRosterView(root, now, state);
|
|
1116
1154
|
const team = teamRosterView(root, { now, rosterState: state });
|
|
1155
|
+
const expiring = expiringWorkers(jobs);
|
|
1117
1156
|
return {
|
|
1118
1157
|
jobs,
|
|
1158
|
+
expiring,
|
|
1159
|
+
renew_command: expiring.length ? RENEW_COMMAND : null,
|
|
1119
1160
|
team: team.rows,
|
|
1120
1161
|
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1121
1162
|
files: {
|
|
@@ -1127,12 +1168,52 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1127
1168
|
};
|
|
1128
1169
|
}
|
|
1129
1170
|
|
|
1130
|
-
|
|
1171
|
+
// Every worker, in any job, whose end date is today or within the next
|
|
1172
|
+
// seven days. Workers already past their date show on their own lines. A
|
|
1173
|
+
// job set for this shell only is skipped: roster confirm does not renew it.
|
|
1174
|
+
function expiringWorkers(jobs) {
|
|
1175
|
+
const list = [];
|
|
1176
|
+
for (const row of jobs || []) {
|
|
1177
|
+
if (row.session_pick) continue;
|
|
1178
|
+
for (const worker of row.workers || []) {
|
|
1179
|
+
if (!worker.expiring_soon) continue;
|
|
1180
|
+
list.push({
|
|
1181
|
+
job: row.job,
|
|
1182
|
+
engine: worker.engine,
|
|
1183
|
+
model: worker.model,
|
|
1184
|
+
tool: worker.runs ? worker.runs.text : worker.engine,
|
|
1185
|
+
until: worker.until,
|
|
1186
|
+
ends_in_days: worker.ends_in_days,
|
|
1187
|
+
from: row.from,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return list;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function untilText(worker, days = null) {
|
|
1131
1195
|
const until = parseRosterUntil(worker.until);
|
|
1132
|
-
if (until) return `until ${until
|
|
1196
|
+
if (until) return `until ${shortDay(worker.until)}${endsSoon(days) ? `, ${endsInText(days)}` : ''}`;
|
|
1133
1197
|
return worker.never_expires ? 'no end date' : 'until no valid date';
|
|
1134
1198
|
}
|
|
1135
1199
|
|
|
1200
|
+
// One line under the jobs when any worker ends within seven days, naming
|
|
1201
|
+
// each one and the command that keeps it going.
|
|
1202
|
+
function renderExpiringLine(expiring) {
|
|
1203
|
+
if (!expiring || !expiring.length) return '';
|
|
1204
|
+
const names = expiring.map((item) => `${item.tool} on ${item.job} ${endsInText(item.ends_in_days)}`);
|
|
1205
|
+
const them = expiring.length === 1 ? 'it' : 'them';
|
|
1206
|
+
return `heads up: ${names.join(', ')}. to keep ${them} 30 more days, run: ${RENEW_COMMAND}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// The same heads-up, short enough for one boot line.
|
|
1210
|
+
function bootExpiringLine(expiring) {
|
|
1211
|
+
if (!expiring || !expiring.length) return '';
|
|
1212
|
+
const [first] = expiring;
|
|
1213
|
+
const more = expiring.length > 1 ? ` and ${expiring.length - 1} more` : '';
|
|
1214
|
+
return `${first.tool} on ${first.job} ${endsInText(first.ends_in_days)}${more}. renew: ${RENEW_COMMAND}`;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1136
1217
|
// Every worker under its job, numbered in order, when the job has more than
|
|
1137
1218
|
// the lead and one backup (the job's own line already shows those two).
|
|
1138
1219
|
function renderWorkerLines(row) {
|
|
@@ -1140,8 +1221,11 @@ function renderWorkerLines(row) {
|
|
|
1140
1221
|
const width = Math.max(...row.workers.map((worker) => (worker.runs ? worker.runs.text : worker.text || '').length));
|
|
1141
1222
|
return row.workers.map((worker, index) => {
|
|
1142
1223
|
const what = (worker.runs ? worker.runs.text : worker.text || '').padEnd(width);
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1224
|
+
const ended = worker.status === 'skipped' && worker.why === 'expired' && worker.ends_in_days !== null;
|
|
1225
|
+
const until = !worker.until || ended ? '' : `until ${worker.until}${worker.expiring_soon ? `, ${endsInText(worker.ends_in_days)}` : ''}`;
|
|
1226
|
+
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.prep ? `prepped by ${worker.prep}` : '', until].filter(Boolean);
|
|
1227
|
+
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup'
|
|
1228
|
+
: ended ? `ended ${shortDay(worker.until)}, skipped` : `skipped, ${worker.why}`;
|
|
1145
1229
|
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1146
1230
|
});
|
|
1147
1231
|
}
|
|
@@ -1165,9 +1249,10 @@ function renderJobRoster(rows) {
|
|
|
1165
1249
|
const owner = row.runs.text.padEnd(ownerWidth);
|
|
1166
1250
|
const backup = row.backup_runs ? `backup ${row.backup_runs.text}${row.backup_prep ? ` prepped by ${row.backup_prep}` : ''}` : 'no backup';
|
|
1167
1251
|
const cap = `${row.max_seconds ? `${rosterMaxText(row.max_seconds)}, ` : ''}${row.prep ? `prepped by ${row.prep}, ` : ''}`;
|
|
1168
|
-
const date = untilText(row.pick);
|
|
1252
|
+
const date = untilText(row.pick, row.ends_in_days);
|
|
1169
1253
|
const fallback = row.lead === 'backup' ? 'using backup' : row.lead === 'later' ? `using ${nowText}` : fallsTo;
|
|
1170
|
-
const
|
|
1254
|
+
const ended = row.ends_in_days !== null && row.ends_in_days !== undefined ? `ended ${shortDay(row.pick.until)}, skipped` : 'expired';
|
|
1255
|
+
const status = row.status === 'expired' ? `${ended}, ${fallback}`
|
|
1171
1256
|
: row.status === 'cooling' ? `${row.cooling}, ${fallback}`
|
|
1172
1257
|
: row.status === 'not ready' ? `not ready, ${fallback}`
|
|
1173
1258
|
: date;
|
|
@@ -1195,7 +1280,8 @@ function renderRosterWarnings(warnings) {
|
|
|
1195
1280
|
}
|
|
1196
1281
|
|
|
1197
1282
|
function renderRosterReport(report) {
|
|
1198
|
-
|
|
1283
|
+
const jobs = [renderJobRoster(report.jobs), renderExpiringLine(report.expiring)].filter(Boolean).join('\n');
|
|
1284
|
+
return [jobs, renderTeamRoster(report.team), renderRosterWarnings(report.warnings)]
|
|
1199
1285
|
.filter(Boolean)
|
|
1200
1286
|
.join('\n\n');
|
|
1201
1287
|
}
|
|
@@ -1530,9 +1616,30 @@ function parseSetFlag(args) {
|
|
|
1530
1616
|
return '';
|
|
1531
1617
|
}
|
|
1532
1618
|
|
|
1533
|
-
|
|
1619
|
+
// One worker as a person reads it: the tool, then its model and effort.
|
|
1620
|
+
function notesWorkerText(worker) {
|
|
1621
|
+
const details = [worker.model, worker.effort ? `effort ${worker.effort}` : '', worker.installed ? '' : 'not installed'].filter(Boolean);
|
|
1622
|
+
return details.length ? `${worker.engine} (${details.join(', ')})` : worker.engine;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// The notes job: the roster's pick when one is set, else the built-in
|
|
1626
|
+
// default. Always answers, so scripts can call it with a short timeout.
|
|
1627
|
+
function runNotesResolve(json, root, now) {
|
|
1628
|
+
const answer = resolveNotesJob(root, { now });
|
|
1629
|
+
if (json) {
|
|
1630
|
+
console.log(JSON.stringify(answer));
|
|
1631
|
+
return 0;
|
|
1632
|
+
}
|
|
1633
|
+
const backup = answer.backup.length ? `backup ${answer.backup.map(notesWorkerText).join(', then ')}` : 'no backup';
|
|
1634
|
+
const where = answer.source === 'roster' ? `from ${answer.from}` : `built-in default${answer.skipped ? ` (${answer.skipped})` : ''}`;
|
|
1635
|
+
console.log(`notes: ${notesWorkerText(answer)}, ${backup}, ${where}`);
|
|
1636
|
+
return 0;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function runResolveCommand(args, root, now) {
|
|
1534
1640
|
const json = args.includes('--json');
|
|
1535
1641
|
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
1642
|
+
if (role && rosterJobKey(role) === 'notes') return runNotesResolve(json, root, now);
|
|
1536
1643
|
if (!role) {
|
|
1537
1644
|
const message = `usage: atris engine resolve <role> or <job>; roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or your own`;
|
|
1538
1645
|
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
@@ -2133,7 +2240,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2133
2240
|
}
|
|
2134
2241
|
|
|
2135
2242
|
if (sub === 'resolve') {
|
|
2136
|
-
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root);
|
|
2243
|
+
return runResolveCommand(scope.args.slice(scope.args.indexOf('resolve') + 1), root, deps.now);
|
|
2137
2244
|
}
|
|
2138
2245
|
|
|
2139
2246
|
if (sub === 'health') {
|
|
@@ -2160,7 +2267,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2160
2267
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
2161
2268
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
2162
2269
|
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');
|
|
2270
|
+
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 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
2271
|
return 0;
|
|
2165
2272
|
}
|
|
2166
2273
|
if (isFreshWorkspace(root)) {
|
|
@@ -2210,6 +2317,8 @@ module.exports = {
|
|
|
2210
2317
|
roster,
|
|
2211
2318
|
rosterReport,
|
|
2212
2319
|
jobRosterView,
|
|
2320
|
+
expiringWorkers,
|
|
2321
|
+
bootExpiringLine,
|
|
2213
2322
|
engineRegistryFile,
|
|
2214
2323
|
readEngineRegistry,
|
|
2215
2324
|
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) {
|
package/commands/improve.js
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* fallback, and scorecard writes can all be faked in tests.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const crypto = require('crypto');
|
|
21
22
|
const fs = require('fs');
|
|
22
23
|
const os = require('os');
|
|
23
24
|
const path = require('path');
|
|
24
25
|
const { spawnSync } = require('child_process');
|
|
26
|
+
const gitSpawn = require('../lib/git-spawn');
|
|
27
|
+
const ignoredCache = require('../lib/ignored-cache');
|
|
25
28
|
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
26
29
|
const { loadCredentials } = require('../utils/auth');
|
|
27
30
|
const pulse = require('../lib/pulse');
|
|
@@ -1085,8 +1088,9 @@ function parseRevisionsArgs(argv = []) {
|
|
|
1085
1088
|
return opts;
|
|
1086
1089
|
}
|
|
1087
1090
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1091
|
+
// Through the module object so tests can count git spawns with a spy.
|
|
1092
|
+
function gitLines(cwdRoot, gitArgs, { input, maxBuffer } = {}) {
|
|
1093
|
+
const r = gitSpawn.runGit(gitArgs, { cwd: cwdRoot, check: false, input, maxBuffer });
|
|
1090
1094
|
if (r.status !== 0) {
|
|
1091
1095
|
const err = new Error(String(r.stderr || `git ${gitArgs[0]} exited ${r.status}`).trim());
|
|
1092
1096
|
err.gitFailed = true;
|
|
@@ -1095,17 +1099,129 @@ function gitLines(cwdRoot, gitArgs) {
|
|
|
1095
1099
|
return String(r.stdout || '');
|
|
1096
1100
|
}
|
|
1097
1101
|
|
|
1102
|
+
// File names come back NUL-separated and never quoted, whatever core.quotePath
|
|
1103
|
+
// says, so a name reads the same fresh from git and from the saved cache.
|
|
1104
|
+
const NAME_ARGS = ['-c', 'core.quotePath=false'];
|
|
1105
|
+
|
|
1106
|
+
function splitNames(out) {
|
|
1107
|
+
return String(out || '').split('\0').filter(Boolean);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1098
1110
|
/**
|
|
1099
1111
|
* Files changed by one commit. Merge commits are attributed by their
|
|
1100
1112
|
* first-parent diff (what the merge actually brought onto the mainline);
|
|
1101
1113
|
* plain commits use diff-tree. Root commits list their initial files.
|
|
1114
|
+
* The rename choice is spelled out so a user's diff.renames setting cannot
|
|
1115
|
+
* change the lists: merges find renames (git diff's default), plain commits
|
|
1116
|
+
* do not (diff-tree's default).
|
|
1102
1117
|
*/
|
|
1103
1118
|
function commitFiles(cwdRoot, commit) {
|
|
1104
1119
|
const parents = commit.parents;
|
|
1105
1120
|
const out = parents.length >= 2
|
|
1106
|
-
? gitLines(cwdRoot, ['diff', '--name-only', `${commit.hash}^1`, commit.hash])
|
|
1107
|
-
: gitLines(cwdRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', '--root', commit.hash]);
|
|
1108
|
-
return out
|
|
1121
|
+
? gitLines(cwdRoot, [...NAME_ARGS, 'diff', '--name-only', '--find-renames', '-z', `${commit.hash}^1`, commit.hash])
|
|
1122
|
+
: gitLines(cwdRoot, [...NAME_ARGS, 'diff-tree', '--no-commit-id', '--name-only', '--no-renames', '-z', '-r', '--root', commit.hash]);
|
|
1123
|
+
return splitNames(out);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Files changed by many plain commits in one git process: the same diff-tree
|
|
1128
|
+
* per-commit call above, fed every hash on stdin. --always prints the hash
|
|
1129
|
+
* even for an empty commit. Raw output frames every file as a ':'-led status
|
|
1130
|
+
* field followed by its path, so a path is never mistaken for a commit hash,
|
|
1131
|
+
* even a path spelled exactly like the next one.
|
|
1132
|
+
*/
|
|
1133
|
+
function plainCommitFiles(cwdRoot, commits) {
|
|
1134
|
+
const found = new Map(commits.map((commit) => [commit.hash, []]));
|
|
1135
|
+
if (!commits.length) return found;
|
|
1136
|
+
const hashes = commits.map((commit) => commit.hash);
|
|
1137
|
+
const out = gitLines(cwdRoot, [...NAME_ARGS, 'diff-tree', '--stdin', '--always', '--raw', '--no-renames', '-z', '-r', '--root'], {
|
|
1138
|
+
input: `${hashes.join('\n')}\n`,
|
|
1139
|
+
// Many commits' lists in one reply; the per-commit calls each had 1MB.
|
|
1140
|
+
maxBuffer: Math.max(1, hashes.length) * 1024 * 1024,
|
|
1141
|
+
});
|
|
1142
|
+
const fields = splitNames(out);
|
|
1143
|
+
let current = null;
|
|
1144
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1145
|
+
const field = fields[i];
|
|
1146
|
+
if (field.startsWith(':')) {
|
|
1147
|
+
// Status field; with --no-renames exactly one path follows it.
|
|
1148
|
+
if (current && i + 1 < fields.length) current.push(fields[i + 1]);
|
|
1149
|
+
i += 1;
|
|
1150
|
+
} else {
|
|
1151
|
+
current = found.get(field) || null;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return found;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Where git keeps state shared by every checkout of this repo (a linked
|
|
1158
|
+
// worktree points at its main repo). null when it cannot be found on disk.
|
|
1159
|
+
function gitCommonDir(root) {
|
|
1160
|
+
if (process.env.GIT_DIR || process.env.GIT_COMMON_DIR) return null;
|
|
1161
|
+
let dir = path.resolve(root);
|
|
1162
|
+
for (;;) {
|
|
1163
|
+
const dotGit = path.join(dir, '.git');
|
|
1164
|
+
let stat = null;
|
|
1165
|
+
try { stat = fs.statSync(dotGit); } catch { stat = null; }
|
|
1166
|
+
if (stat && stat.isDirectory()) return dotGit;
|
|
1167
|
+
if (stat && stat.isFile()) {
|
|
1168
|
+
const match = fs.readFileSync(dotGit, 'utf8').match(/^gitdir:\s*(.+)$/m);
|
|
1169
|
+
if (!match) return null;
|
|
1170
|
+
const gitDir = path.resolve(dir, match[1].trim());
|
|
1171
|
+
try {
|
|
1172
|
+
return path.resolve(gitDir, fs.readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim());
|
|
1173
|
+
} catch {
|
|
1174
|
+
return gitDir;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const parent = path.dirname(dir);
|
|
1178
|
+
if (parent === dir) return null;
|
|
1179
|
+
dir = parent;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// A shallow clone cuts history at boundary commits, and those commits list
|
|
1184
|
+
// different files once the history is fetched. The saved lists are only good
|
|
1185
|
+
// for the shallow state they were read under; null means do not cache.
|
|
1186
|
+
function shallowStamp(root) {
|
|
1187
|
+
const common = gitCommonDir(root);
|
|
1188
|
+
if (!common) return null;
|
|
1189
|
+
try {
|
|
1190
|
+
return crypto.createHash('sha1').update(fs.readFileSync(path.join(common, 'shallow'))).digest('hex');
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
return error.code === 'ENOENT' ? 'full' : null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// A commit's file list never changes, so it is saved by hash in the ignored
|
|
1197
|
+
// .atris/cache and the gauge (shown on every boot) only asks git about commits
|
|
1198
|
+
// it has not seen. Hashes this read used come first; older ones stay until the
|
|
1199
|
+
// cap, so a 7-day boot read and a 14-day report read do not evict each other.
|
|
1200
|
+
const REVISION_FILES_CACHE = 'revision-files.json';
|
|
1201
|
+
const REVISION_FILES_CACHE_MAX = 5000;
|
|
1202
|
+
|
|
1203
|
+
function loadRevisionFilesCache(root) {
|
|
1204
|
+
const shallow = ignoredCache.cacheEnabled(root) ? shallowStamp(root) : null;
|
|
1205
|
+
const cache = { root, shallow, entries: {}, used: {}, dirty: false, writable: Boolean(shallow) };
|
|
1206
|
+
if (!cache.writable) return cache;
|
|
1207
|
+
const parsed = ignoredCache.readCacheJson(root, REVISION_FILES_CACHE);
|
|
1208
|
+
if (parsed && parsed.version === 1 && parsed.shallow === shallow && parsed.entries && typeof parsed.entries === 'object') {
|
|
1209
|
+
cache.entries = parsed.entries;
|
|
1210
|
+
}
|
|
1211
|
+
return cache;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function saveRevisionFilesCache(cache) {
|
|
1215
|
+
if (!cache.writable || !cache.dirty) return;
|
|
1216
|
+
const entries = { ...cache.used };
|
|
1217
|
+
let count = Object.keys(entries).length;
|
|
1218
|
+
for (const [hash, files] of Object.entries(cache.entries)) {
|
|
1219
|
+
if (count >= REVISION_FILES_CACHE_MAX) break;
|
|
1220
|
+
if (entries[hash] || !Array.isArray(files)) continue;
|
|
1221
|
+
entries[hash] = files;
|
|
1222
|
+
count += 1;
|
|
1223
|
+
}
|
|
1224
|
+
ignoredCache.writeCacheJson(cache.root, REVISION_FILES_CACHE, { version: 1, shallow: cache.shallow, entries });
|
|
1109
1225
|
}
|
|
1110
1226
|
|
|
1111
1227
|
/**
|
|
@@ -1150,10 +1266,27 @@ function collectRevisionSignals(root, options = {}) {
|
|
|
1150
1266
|
// commits count as revision signals.
|
|
1151
1267
|
const humans = commits.filter((c) => !c.isAgent && c.parents.length < 2);
|
|
1152
1268
|
|
|
1153
|
-
|
|
1269
|
+
// Every landing's files are needed, and a human commit's only when it falls
|
|
1270
|
+
// inside some landing's window. Plain commits missing from the cache are
|
|
1271
|
+
// read in one batch; merges keep their own first-parent diff.
|
|
1272
|
+
const cache = loadRevisionFilesCache(root);
|
|
1273
|
+
const needed = [...landings, ...humans.filter((human) => landings.some((landing) => human.ms > landing.ms && human.ms <= landing.ms + REVISION_WINDOW_MS))];
|
|
1274
|
+
const missing = [];
|
|
1275
|
+
for (const commit of needed) {
|
|
1276
|
+
const hit = cache.entries[commit.hash];
|
|
1277
|
+
if (Array.isArray(hit)) cache.used[commit.hash] = hit;
|
|
1278
|
+
else if (commit.parents.length < 2) missing.push(commit);
|
|
1279
|
+
}
|
|
1280
|
+
for (const [hash, files] of plainCommitFiles(root, missing)) {
|
|
1281
|
+
cache.used[hash] = files;
|
|
1282
|
+
cache.dirty = true;
|
|
1283
|
+
}
|
|
1154
1284
|
const filesOf = (commit) => {
|
|
1155
|
-
if (!
|
|
1156
|
-
|
|
1285
|
+
if (!cache.used[commit.hash]) {
|
|
1286
|
+
cache.used[commit.hash] = commitFiles(root, commit);
|
|
1287
|
+
cache.dirty = true;
|
|
1288
|
+
}
|
|
1289
|
+
return cache.used[commit.hash];
|
|
1157
1290
|
};
|
|
1158
1291
|
|
|
1159
1292
|
const revisions = [];
|
|
@@ -1177,6 +1310,7 @@ function collectRevisionSignals(root, options = {}) {
|
|
|
1177
1310
|
});
|
|
1178
1311
|
}
|
|
1179
1312
|
}
|
|
1313
|
+
saveRevisionFilesCache(cache);
|
|
1180
1314
|
|
|
1181
1315
|
return {
|
|
1182
1316
|
schema: REVISIONS_SCHEMA,
|
package/commands/integrations.js
CHANGED
|
@@ -33,11 +33,12 @@ const {
|
|
|
33
33
|
const fs = require('fs');
|
|
34
34
|
const os = require('os');
|
|
35
35
|
const path = require('path');
|
|
36
|
+
const { stateHome } = require('../lib/state-home');
|
|
36
37
|
const readline = require('readline');
|
|
37
38
|
const { spawnSync } = require('child_process');
|
|
38
39
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
39
40
|
|
|
40
|
-
const CALENDAR_CACHE_PATH = path.join(
|
|
41
|
+
const CALENDAR_CACHE_PATH = path.join(stateHome(), '.atris', 'calendar-events-cache.json');
|
|
41
42
|
const GMAIL_CONNECT_POLL_MS = 3000;
|
|
42
43
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
43
44
|
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
@@ -182,7 +183,7 @@ function parseGmailVerdictsArgs(args = []) {
|
|
|
182
183
|
|
|
183
184
|
function gmailAccountStatePath() {
|
|
184
185
|
return process.env.ATRIS_GMAIL_ACCOUNT_FILE
|
|
185
|
-
|| path.join(
|
|
186
|
+
|| path.join(stateHome(), '.atris', 'gmail-account.json');
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
function readGmailStickyAccount() {
|
|
@@ -1794,7 +1795,7 @@ function normalizeContactLabel(value) {
|
|
|
1794
1795
|
}
|
|
1795
1796
|
|
|
1796
1797
|
function imessageLookupCachePath() {
|
|
1797
|
-
return path.join(
|
|
1798
|
+
return path.join(stateHome(), '.atris', 'cache', 'imessage-contacts.json');
|
|
1798
1799
|
}
|
|
1799
1800
|
|
|
1800
1801
|
function readImessageLookupCache() {
|
package/commands/loops.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { stateHome } = require('../lib/state-home');
|
|
16
17
|
const { execSync, spawnSync } = require('child_process');
|
|
17
18
|
|
|
18
19
|
const MAX_LOG_AGE_DAYS = 14;
|
|
@@ -26,7 +27,7 @@ const SCAFFOLD_FILES = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
function heartbeatDir() {
|
|
29
|
-
return process.env.ATRIS_HEARTBEAT_DIR || path.join(
|
|
30
|
+
return process.env.ATRIS_HEARTBEAT_DIR || path.join(stateHome(), '.atris', 'heartbeat');
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function readJson(filePath, fallback) {
|