atris 3.35.0 → 3.36.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.
Files changed (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +565 -265
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3029 -339
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +79 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
@@ -16,34 +16,66 @@
16
16
  const fs = require('fs');
17
17
  const os = require('os');
18
18
  const path = require('path');
19
+ const readline = require('readline');
19
20
  const { spawnSync } = require('child_process');
21
+ const { Writable } = require('stream');
20
22
  const {
21
23
  RUNNER_PROFILE_DEFS,
22
- RUNNER_PROFILE_ALIASES,
23
24
  RUNNER_PROFILE_NAMES,
24
25
  buildRunnerCommand,
25
26
  } = require('../lib/runner-command');
27
+ const {
28
+ ENGINE_ROLES,
29
+ ENGINE_HEALTH_STATUSES,
30
+ binInstalled,
31
+ canonicalEngineName,
32
+ engineRegistryFile,
33
+ engineRegistryView,
34
+ readEngineRegistry,
35
+ resolveEngineForRole,
36
+ setEngineHealth,
37
+ } = require('../lib/engine-registry');
26
38
  const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
39
+ const { ensureValidCredentials } = require('../utils/auth');
40
+ const { apiRequestJson } = require('../utils/api');
27
41
 
28
42
  const HOUSE_ENGINE = 'atris-fast';
43
+ const MAX_ENGINE_LOGIN_FILE_BYTES = 64 * 1024;
44
+ const ENGINE_DEVICE_LOGIN_POLL_MS = 3000;
45
+ const ENGINE_DEVICE_LOGIN_TIMEOUT_MS = 16 * 60 * 1000;
46
+ const ENGINE_LOGIN_MANIFESTS = Object.freeze({
47
+ codex: Object.freeze({
48
+ type: 'files',
49
+ files: Object.freeze(['~/.codex/auth.json']),
50
+ missingHint: 'run codex login first',
51
+ }),
52
+ claude: Object.freeze({
53
+ type: 'files',
54
+ files: Object.freeze(['~/.claude/.credentials.json']),
55
+ missingHint: 'run claude login first',
56
+ }),
57
+ cursor: Object.freeze({
58
+ type: 'files',
59
+ files: Object.freeze(['~/.cursor/cli-config.json']),
60
+ missingHint: 'run cursor login first',
61
+ }),
62
+ devin: Object.freeze({
63
+ type: 'api_key',
64
+ missingHint: 'paste a Devin API key',
65
+ }),
66
+ grok: Object.freeze({
67
+ type: 'files',
68
+ files: Object.freeze(['~/.grok/auth.json']),
69
+ missingHint: 'run grok and log in with grok.com',
70
+ }),
71
+ });
29
72
 
30
- function engineFile(root = process.cwd()) {
31
- return path.join(root, '.atris', 'engine.json');
32
- }
33
-
34
- function binInstalled(bin) {
35
- const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
36
- if (!safe) return false;
37
- const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
38
- return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
73
+ function knownLoginProviders() {
74
+ return Object.keys(ENGINE_LOGIN_MANIFESTS);
39
75
  }
40
76
 
41
- function canonicalEngineName(name) {
42
- const trimmed = String(name || '').trim();
43
- if (!trimmed) return '';
44
- if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
45
- if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
46
- return '';
77
+ function engineFile(root = process.cwd()) {
78
+ return path.join(root, '.atris', 'engine.json');
47
79
  }
48
80
 
49
81
  function readSavedEngine(root = process.cwd()) {
@@ -70,11 +102,9 @@ function resolveDefaultEngine(root = process.cwd()) {
70
102
 
71
103
  function roster(root = process.cwd()) {
72
104
  const current = resolveDefaultEngine(root);
73
- return RUNNER_PROFILE_NAMES.map((name) => ({
74
- name,
75
- bin: RUNNER_PROFILE_DEFS[name].bin,
76
- installed: binInstalled(RUNNER_PROFILE_DEFS[name].bin),
77
- default: name === current.name,
105
+ return engineRegistryView(root).map((engine) => ({
106
+ ...engine,
107
+ default: engine.id === current.name,
78
108
  }));
79
109
  }
80
110
 
@@ -93,6 +123,764 @@ function resetEngine(root = process.cwd()) {
93
123
  try { fs.unlinkSync(engineFile(root)); return true; } catch { return false; }
94
124
  }
95
125
 
126
+ function expandHomePath(filePath, homeDir = os.homedir()) {
127
+ const value = String(filePath || '');
128
+ if (value === '~') return homeDir;
129
+ if (value.startsWith('~/')) return path.join(homeDir, value.slice(2));
130
+ return value;
131
+ }
132
+
133
+ function normalizeLoginProvider(name) {
134
+ const raw = String(name || '').trim().toLowerCase();
135
+ if (!raw) return '';
136
+ const canonical = canonicalEngineName(raw) || raw;
137
+ return ENGINE_LOGIN_MANIFESTS[canonical] ? canonical : '';
138
+ }
139
+
140
+ function normalizeEngineLoginSeat(name) {
141
+ return String(name || '')
142
+ .trim()
143
+ .toUpperCase()
144
+ .replace(/[\s-]+/g, '_');
145
+ }
146
+
147
+ function validEngineLoginSeat(name) {
148
+ return /^[A-Z0-9][A-Z0-9_]{0,47}$/.test(String(name || ''));
149
+ }
150
+
151
+ function engineLoginSeatError() {
152
+ return 'seat names are letters, numbers, underscores - like personal or work';
153
+ }
154
+
155
+ function byteLength(value) {
156
+ return Buffer.byteLength(String(value || ''), 'utf8');
157
+ }
158
+
159
+ function detectedEmailFromJsonValue(value, depth = 0) {
160
+ if (depth > 6 || value == null) return '';
161
+ if (typeof value === 'string') {
162
+ const match = value.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
163
+ return match ? match[0] : '';
164
+ }
165
+ if (Array.isArray(value)) {
166
+ for (const item of value) {
167
+ const found = detectedEmailFromJsonValue(item, depth + 1);
168
+ if (found) return found;
169
+ }
170
+ return '';
171
+ }
172
+ if (typeof value === 'object') {
173
+ const preferred = ['email', 'account_email', 'user_email'];
174
+ for (const key of preferred) {
175
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
176
+ const found = detectedEmailFromJsonValue(value[key], depth + 1);
177
+ if (found) return found;
178
+ }
179
+ }
180
+ for (const item of Object.values(value)) {
181
+ const found = detectedEmailFromJsonValue(item, depth + 1);
182
+ if (found) return found;
183
+ }
184
+ }
185
+ return '';
186
+ }
187
+
188
+ function detectedEmailFromContent(content) {
189
+ try {
190
+ return detectedEmailFromJsonValue(JSON.parse(content));
191
+ } catch {
192
+ return '';
193
+ }
194
+ }
195
+
196
+ function readEngineLoginFiles(provider, { homeDir = os.homedir(), fsModule = fs } = {}) {
197
+ const manifest = ENGINE_LOGIN_MANIFESTS[provider];
198
+ if (!manifest || manifest.type !== 'files') {
199
+ throw new Error(`No file manifest for ${provider}`);
200
+ }
201
+
202
+ const files = {};
203
+ const summary = [];
204
+ for (const displayPath of manifest.files) {
205
+ const absolutePath = expandHomePath(displayPath, homeDir);
206
+ let stat;
207
+ try {
208
+ stat = fsModule.statSync(absolutePath);
209
+ } catch {
210
+ const err = new Error(`Missing ${displayPath}. ${manifest.missingHint}.`);
211
+ err.code = 'missing_file';
212
+ throw err;
213
+ }
214
+ if (!stat.isFile()) {
215
+ const err = new Error(`Missing ${displayPath}. ${manifest.missingHint}.`);
216
+ err.code = 'missing_file';
217
+ throw err;
218
+ }
219
+ if (stat.size > MAX_ENGINE_LOGIN_FILE_BYTES) {
220
+ const err = new Error(`${displayPath} is ${stat.size} bytes; maximum is ${MAX_ENGINE_LOGIN_FILE_BYTES} bytes.`);
221
+ err.code = 'file_too_large';
222
+ throw err;
223
+ }
224
+ const content = fsModule.readFileSync(absolutePath, 'utf8');
225
+ files[displayPath] = content;
226
+ summary.push({
227
+ path: displayPath,
228
+ bytes: byteLength(content),
229
+ email: detectedEmailFromContent(content),
230
+ });
231
+ }
232
+ return { payload: { files }, summary };
233
+ }
234
+
235
+ function parseEngineLoginArgs(args = []) {
236
+ const options = {
237
+ provider: '',
238
+ list: false,
239
+ remove: '',
240
+ yes: false,
241
+ json: false,
242
+ computer: false,
243
+ business: '',
244
+ businessFlag: false,
245
+ seat: '',
246
+ seatFlag: false,
247
+ help: false,
248
+ };
249
+ for (let i = 0; i < args.length; i += 1) {
250
+ const arg = String(args[i] || '');
251
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
252
+ options.help = true;
253
+ continue;
254
+ }
255
+ if (arg === '--list' || arg === 'list') {
256
+ options.list = true;
257
+ continue;
258
+ }
259
+ if (arg === '--remove' && args[i + 1]) {
260
+ options.remove = String(args[i + 1] || '').trim();
261
+ i += 1;
262
+ continue;
263
+ }
264
+ if (arg.startsWith('--remove=')) {
265
+ options.remove = arg.slice('--remove='.length).trim();
266
+ continue;
267
+ }
268
+ if (arg === '--yes' || arg === '-y') {
269
+ options.yes = true;
270
+ continue;
271
+ }
272
+ if (arg === '--json') {
273
+ options.json = true;
274
+ continue;
275
+ }
276
+ if (arg === '--computer') {
277
+ options.computer = true;
278
+ continue;
279
+ }
280
+ if (arg === '--business' || arg === '-b') {
281
+ options.computer = true;
282
+ options.businessFlag = true;
283
+ const next = args[i + 1] === undefined ? '' : String(args[i + 1] || '');
284
+ if (next && !next.startsWith('--')) {
285
+ options.business = next.trim();
286
+ i += 1;
287
+ }
288
+ continue;
289
+ }
290
+ if (arg.startsWith('--business=')) {
291
+ options.computer = true;
292
+ options.businessFlag = true;
293
+ options.business = arg.slice('--business='.length).trim();
294
+ continue;
295
+ }
296
+ if (arg === '--seat') {
297
+ options.seatFlag = true;
298
+ const next = args[i + 1] === undefined ? '' : String(args[i + 1] || '');
299
+ if (next && !next.startsWith('--')) {
300
+ options.seat = normalizeEngineLoginSeat(next);
301
+ i += 1;
302
+ }
303
+ continue;
304
+ }
305
+ if (arg.startsWith('--seat=')) {
306
+ options.seatFlag = true;
307
+ options.seat = normalizeEngineLoginSeat(arg.slice('--seat='.length));
308
+ continue;
309
+ }
310
+ if (arg.startsWith('--')) continue;
311
+ if (!options.provider) options.provider = arg;
312
+ }
313
+ return options;
314
+ }
315
+
316
+ function parseEngineSeedArgs(args = []) {
317
+ const options = {
318
+ provider: '',
319
+ business: '',
320
+ user: false,
321
+ json: false,
322
+ help: false,
323
+ };
324
+ for (let i = 0; i < args.length; i += 1) {
325
+ const arg = String(args[i] || '');
326
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
327
+ options.help = true;
328
+ continue;
329
+ }
330
+ if ((arg === '--business' || arg === '-b') && args[i + 1]) {
331
+ options.business = String(args[i + 1] || '').trim();
332
+ i += 1;
333
+ continue;
334
+ }
335
+ if (arg.startsWith('--business=')) {
336
+ options.business = arg.slice('--business='.length).trim();
337
+ continue;
338
+ }
339
+ if (arg === '--user') {
340
+ options.user = true;
341
+ continue;
342
+ }
343
+ if (arg === '--json') {
344
+ options.json = true;
345
+ continue;
346
+ }
347
+ if (arg.startsWith('--')) continue;
348
+ if (!options.provider) options.provider = arg;
349
+ }
350
+ return options;
351
+ }
352
+
353
+ function printEngineLoginHelp() {
354
+ console.log('\n atris engine login <provider> --yes\n upload a local whitelisted engine login to the Atris vault\n atris engine login <provider> --computer [--seat <name>]\n sign in on one of your Atris computers by device flow\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on a business Atris computer by device flow\n atris engine login --list\n list vaulted engine logins\n atris engine login --remove <provider>\n remove a vaulted engine login\n providers: codex, claude, cursor, devin, grok\n');
355
+ }
356
+
357
+ function printEngineSeedHelp() {
358
+ console.log('\n atris engine seed <provider> --business <id>\n atris engine seed <provider> --user\n ask the backend to seed a vaulted login onto a computer\n');
359
+ }
360
+
361
+ function redactBackendResponse(value, parentKey = '') {
362
+ if (value == null) return value;
363
+ if (Array.isArray(value)) return value.map((item) => redactBackendResponse(item, parentKey));
364
+ if (typeof value !== 'object') {
365
+ if (/api[_-]?key|token|secret|credential|auth|files?/i.test(parentKey)) return '[redacted]';
366
+ return value;
367
+ }
368
+ const output = {};
369
+ for (const [key, item] of Object.entries(value)) {
370
+ if (/api[_-]?key|token|secret|credential|auth/i.test(key)) {
371
+ output[key] = '[redacted]';
372
+ } else if (key === 'files' && item && typeof item === 'object') {
373
+ output[key] = Object.fromEntries(Object.keys(item).map((filePath) => [filePath, '[redacted]']));
374
+ } else {
375
+ output[key] = redactBackendResponse(item, key);
376
+ }
377
+ }
378
+ return output;
379
+ }
380
+
381
+ function printBackendResult(result, { json = false } = {}) {
382
+ const data = result && result.data !== undefined && result.data !== null
383
+ ? result.data
384
+ : { ok: Boolean(result && result.ok), status: result && result.status };
385
+ const safe = redactBackendResponse(data);
386
+ if (json || typeof safe === 'object') {
387
+ console.log(JSON.stringify(safe, null, 2));
388
+ } else {
389
+ console.log(String(safe));
390
+ }
391
+ }
392
+
393
+ function printLoginSummary(provider, summary) {
394
+ console.log('');
395
+ console.log(` engine login: ${provider}`);
396
+ for (const item of summary) {
397
+ const email = item.email ? ` email: ${item.email}` : '';
398
+ console.log(` ${item.path.padEnd(32)} ${String(item.bytes).padStart(6)} bytes${email}`);
399
+ }
400
+ console.log('');
401
+ }
402
+
403
+ function readLineNoEcho(question) {
404
+ return new Promise((resolve) => {
405
+ if (!process.stdin.isTTY) {
406
+ let data = '';
407
+ process.stdout.write(question);
408
+ process.stdin.setEncoding('utf8');
409
+ process.stdin.on('data', (chunk) => { data += chunk; });
410
+ process.stdin.on('end', () => resolve(data.trim()));
411
+ process.stdin.on('error', () => resolve(''));
412
+ process.stdin.resume();
413
+ return;
414
+ }
415
+
416
+ const muted = new Writable({
417
+ write(chunk, encoding, callback) {
418
+ if (!muted.muted) process.stdout.write(chunk, encoding);
419
+ callback();
420
+ },
421
+ });
422
+ const rl = readline.createInterface({ input: process.stdin, output: muted, terminal: true });
423
+ rl.question(question, (answer) => {
424
+ rl.close();
425
+ process.stdout.write('\n');
426
+ resolve(String(answer || '').trim());
427
+ });
428
+ muted.muted = true;
429
+ });
430
+ }
431
+
432
+ function readLineVisible(question) {
433
+ return new Promise((resolve) => {
434
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
435
+ rl.question(question, (answer) => {
436
+ rl.close();
437
+ resolve(String(answer || '').trim());
438
+ });
439
+ });
440
+ }
441
+
442
+ async function promptApiKey(provider, deps = {}) {
443
+ if (typeof deps.promptSecret === 'function') {
444
+ return String(await deps.promptSecret(provider) || '').trim();
445
+ }
446
+ return readLineNoEcho(`Paste ${provider} API key: `);
447
+ }
448
+
449
+ async function confirmEngineLoginUpload(provider, deps = {}) {
450
+ if (typeof deps.confirmUpload === 'function') {
451
+ return Boolean(await deps.confirmUpload(provider));
452
+ }
453
+ const answer = await readLineVisible('Upload this credential to the Atris backend vault? [y/N] ');
454
+ return /^y(es)?$/i.test(answer);
455
+ }
456
+
457
+ async function authenticatedEngineApi(pathname, options, deps = {}) {
458
+ const apiFn = deps.apiRequestJson || apiRequestJson;
459
+ const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
460
+ const ensured = await ensureFn(apiFn);
461
+ if (ensured.error) {
462
+ return {
463
+ ok: false,
464
+ status: 0,
465
+ data: null,
466
+ error: ensured.detail || ensured.error || 'not_logged_in',
467
+ authError: true,
468
+ };
469
+ }
470
+ const token = ensured.credentials && ensured.credentials.token;
471
+ return apiFn(pathname, {
472
+ ...options,
473
+ token,
474
+ });
475
+ }
476
+
477
+ async function listEngineLogins(deps = {}) {
478
+ return authenticatedEngineApi('/engines/logins', {
479
+ method: 'GET',
480
+ timeoutMs: 15000,
481
+ retries: 0,
482
+ }, deps);
483
+ }
484
+
485
+ async function listEngineSeats(deps = {}) {
486
+ return authenticatedEngineApi('/engines/logins/seats', {
487
+ method: 'GET',
488
+ timeoutMs: 15000,
489
+ retries: 0,
490
+ }, deps);
491
+ }
492
+
493
+ async function readyCheckEngineLogin(provider, target, deps = {}) {
494
+ return authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/ready-check`, {
495
+ method: 'POST',
496
+ body: { target },
497
+ timeoutMs: 30000,
498
+ retries: 0,
499
+ }, deps);
500
+ }
501
+
502
+ function formatEngineSeats(data, now = Date.now()) {
503
+ const seats = Array.isArray(data?.seats) ? data.seats : [];
504
+ if (!seats.length) return 'No accounts linked yet. Run: atris computer setup';
505
+
506
+ const nowValue = now instanceof Date ? now.getTime() : Number(now);
507
+ const nowSeconds = Number.isFinite(nowValue) ? nowValue / 1000 : Date.now() / 1000;
508
+ return seats.map((seat) => {
509
+ const engine = String(seat?.engine || 'engine').trim().toLowerCase();
510
+ const name = String(seat?.name || 'account').trim().toLowerCase();
511
+ const coolingUntil = Number(seat?.cooling_until);
512
+ if (!Number.isFinite(coolingUntil) || coolingUntil <= nowSeconds) {
513
+ return `${engine} ${name} - ready`;
514
+ }
515
+ const minutes = Math.ceil((coolingUntil - nowSeconds) / 60);
516
+ const hours = Math.floor(minutes / 60);
517
+ return `${engine} ${name} - cooling down, back ${hours}h ${minutes % 60}m`;
518
+ }).join('\n');
519
+ }
520
+
521
+ async function runEngineSeatsCommand(deps = {}) {
522
+ const result = await listEngineSeats(deps);
523
+ if (!result.ok) {
524
+ console.error(result.authError ? 'Run atris login first.' : `engine seats failed: ${result.error || result.status}`);
525
+ return 1;
526
+ }
527
+ const now = typeof deps.now === 'function' ? deps.now() : Date.now();
528
+ console.log(formatEngineSeats(result.data, now));
529
+ return 0;
530
+ }
531
+
532
+ async function buildEngineLoginPayload(provider, deps = {}) {
533
+ const manifest = ENGINE_LOGIN_MANIFESTS[provider];
534
+ if (!manifest) {
535
+ throw new Error(`Unknown engine login provider "${provider}". Known providers: ${knownLoginProviders().join(', ')}`);
536
+ }
537
+
538
+ if (manifest.type === 'api_key') {
539
+ const apiKey = await promptApiKey(provider, deps);
540
+ if (!apiKey) {
541
+ const err = new Error('No API key provided.');
542
+ err.code = 'missing_api_key';
543
+ throw err;
544
+ }
545
+ if (byteLength(apiKey) > MAX_ENGINE_LOGIN_FILE_BYTES) {
546
+ const err = new Error(`API key is ${byteLength(apiKey)} bytes; maximum is ${MAX_ENGINE_LOGIN_FILE_BYTES} bytes.`);
547
+ err.code = 'api_key_too_large';
548
+ throw err;
549
+ }
550
+ return {
551
+ payload: { api_key: apiKey },
552
+ summary: [{ path: 'api key', bytes: byteLength(apiKey), email: '' }],
553
+ };
554
+ }
555
+
556
+ return readEngineLoginFiles(provider, deps);
557
+ }
558
+
559
+ function engineDeviceLoginTarget(options) {
560
+ if (options.target) return options.target;
561
+ if (!options.computer) return null;
562
+ if (options.businessFlag && !options.business) {
563
+ const err = new Error('usage: atris engine login <provider> --business <id>');
564
+ err.code = 'usage';
565
+ throw err;
566
+ }
567
+ if (options.business) return { type: 'business', id: options.business };
568
+ return { type: 'user' };
569
+ }
570
+
571
+ function normalizeDeviceLoginStatus(data, provider, sessionId, seat = '') {
572
+ const payload = data && typeof data === 'object' ? data : {};
573
+ return {
574
+ ...payload,
575
+ session_id: payload.session_id || sessionId || '',
576
+ provider: payload.provider || provider,
577
+ ...(payload.seat || seat ? { seat: payload.seat || seat } : {}),
578
+ };
579
+ }
580
+
581
+ function printDeviceLoginCode(status, { json = false } = {}) {
582
+ const lines = [
583
+ '',
584
+ `Sign in: ${status.verify_url}`,
585
+ `Code: ${status.code} (expires in 15 minutes; never share this code)`,
586
+ '',
587
+ ];
588
+ for (const line of lines) {
589
+ if (json) console.error(line);
590
+ else console.log(line);
591
+ }
592
+ }
593
+
594
+ function printDeviceLoginCompleted(status, { json = false } = {}) {
595
+ if (json) {
596
+ console.log(JSON.stringify(status, null, 2));
597
+ return;
598
+ }
599
+ const registered = status.registered === true ? 'true' : status.registered === false ? 'false' : String(status.registered ?? '');
600
+ console.log('');
601
+ console.log(`engine login ready: ${status.provider}`);
602
+ if (status.seat) console.log(`seat: ${status.seat}`);
603
+ console.log(`account_email: ${status.account_email || '(none)'}`);
604
+ console.log(`registered: ${registered}`);
605
+ console.log(`ready-check: provider=${status.provider} session_id=${status.session_id || ''} status=${status.status} registered=${registered}`);
606
+ console.log('');
607
+ }
608
+
609
+ function printDeviceLoginFinalJson(status, options) {
610
+ if (options.json) console.log(JSON.stringify(status, null, 2));
611
+ }
612
+
613
+ async function waitForDeviceLoginPoll(ms, deps = {}) {
614
+ if (typeof deps.sleep === 'function') return deps.sleep(ms);
615
+ return new Promise((resolve) => setTimeout(resolve, ms));
616
+ }
617
+
618
+ async function readDeviceLoginPasteBackCode(deps = {}) {
619
+ const readlineDep = deps.readline || readline;
620
+ const input = deps.stdin || process.stdin;
621
+ const output = deps.stdout || process.stdout;
622
+ const rl = readlineDep.createInterface({ input, output });
623
+ try {
624
+ return String(await new Promise((resolve) => rl.question('', resolve)) || '').trim();
625
+ } finally {
626
+ rl.close();
627
+ }
628
+ }
629
+
630
+ async function runEngineDeviceLoginCommand(provider, options, deps = {}) {
631
+ let target;
632
+ let seat;
633
+ try {
634
+ target = engineDeviceLoginTarget(options);
635
+ seat = normalizeEngineLoginSeat(options.seat);
636
+ if ((options.seatFlag || options.seat) && !validEngineLoginSeat(seat)) {
637
+ const err = new Error(engineLoginSeatError());
638
+ err.code = 'usage';
639
+ throw err;
640
+ }
641
+ } catch (err) {
642
+ console.error(err.message);
643
+ return 2;
644
+ }
645
+
646
+ const body = seat ? { target, seat } : { target };
647
+ const started = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/device-login`, {
648
+ method: 'POST',
649
+ body,
650
+ timeoutMs: 30000,
651
+ retries: 0,
652
+ }, deps);
653
+ if (!started.ok) {
654
+ console.error(options.setup
655
+ ? `could not connect the ${provider} account; please try again.`
656
+ : (started.authError ? 'Run atris login first.' : `engine login device flow failed: ${started.error || started.status}`));
657
+ return 1;
658
+ }
659
+
660
+ const startStatus = normalizeDeviceLoginStatus(started.data, provider, '', seat);
661
+ const sessionId = startStatus.session_id;
662
+ if (!sessionId) {
663
+ console.error(options.setup
664
+ ? `could not connect the ${provider} account; please try again.`
665
+ : 'engine login device flow failed: missing session_id');
666
+ return 1;
667
+ }
668
+
669
+ let finalStatus = startStatus;
670
+ let codePrinted = false;
671
+ let pasteBackSubmitted = false;
672
+ const pollMs = Math.max(1, Number(deps.deviceLoginPollMs ?? ENGINE_DEVICE_LOGIN_POLL_MS));
673
+ const timeoutMs = Math.max(0, Number(deps.deviceLoginTimeoutMs ?? ENGINE_DEVICE_LOGIN_TIMEOUT_MS));
674
+ const maxPolls = Math.ceil(timeoutMs / pollMs);
675
+
676
+ for (let attempt = 0; attempt <= maxPolls; attempt += 1) {
677
+ const polled = await authenticatedEngineApi(`/engines/logins/device-login/${encodeURIComponent(sessionId)}`, {
678
+ method: 'GET',
679
+ timeoutMs: 30000,
680
+ retries: 0,
681
+ }, deps);
682
+ if (!polled.ok) {
683
+ console.error(options.setup
684
+ ? `could not connect the ${provider} account; please try again.`
685
+ : (polled.authError ? 'Run atris login first.' : `engine login device poll failed: ${polled.error || polled.status}`));
686
+ return 1;
687
+ }
688
+
689
+ finalStatus = normalizeDeviceLoginStatus(polled.data, provider, sessionId, seat);
690
+ const status = String(finalStatus.status || '').toLowerCase();
691
+ if (!codePrinted && finalStatus.verify_url && finalStatus.code) {
692
+ if (options.setup) {
693
+ console.log(`On your phone, open: ${finalStatus.verify_url} and type the code: ${finalStatus.code}`);
694
+ } else {
695
+ printDeviceLoginCode(finalStatus, { json: options.json });
696
+ }
697
+ codePrinted = true;
698
+ }
699
+
700
+ const pasteBackRequired = !pasteBackSubmitted
701
+ && status === 'pending_user'
702
+ && finalStatus.verify_url
703
+ && !finalStatus.code
704
+ && String(finalStatus.provider || provider).toLowerCase() === 'claude';
705
+ if (pasteBackRequired) {
706
+ const printPrompt = options.json ? console.error : console.log;
707
+ printPrompt(`On your phone, open: ${finalStatus.verify_url}`);
708
+ let submitted = false;
709
+ for (let submitAttempt = 0; submitAttempt < 2; submitAttempt += 1) {
710
+ printPrompt('When the browser shows you a code, paste it here:');
711
+ const pastedCode = await readDeviceLoginPasteBackCode(deps);
712
+ const codeResult = await authenticatedEngineApi(
713
+ `/engines/logins/device-login/${encodeURIComponent(sessionId)}/code`,
714
+ {
715
+ method: 'POST',
716
+ body: { code: pastedCode },
717
+ timeoutMs: 30000,
718
+ retries: 0,
719
+ },
720
+ deps
721
+ );
722
+ if (codeResult.ok) {
723
+ submitted = true;
724
+ pasteBackSubmitted = true;
725
+ break;
726
+ }
727
+ if (submitAttempt === 0) {
728
+ console.error('that code did not work; paste it again.');
729
+ } else {
730
+ console.error('that code did not work; please start the login again.');
731
+ }
732
+ }
733
+ if (!submitted) return 1;
734
+ }
735
+
736
+ if (status === 'completed') {
737
+ if (options.setup) console.log(`${seat} linked.`);
738
+ else printDeviceLoginCompleted(finalStatus, { json: options.json });
739
+ return 0;
740
+ }
741
+ if (status === 'failed' || status === 'expired') {
742
+ if (options.setup) {
743
+ console.error(`could not connect the ${provider} account; please try again.`);
744
+ } else {
745
+ printDeviceLoginFinalJson(finalStatus, options);
746
+ console.error(`engine login device flow ended: ${status}`);
747
+ }
748
+ return 1;
749
+ }
750
+
751
+ if (attempt < maxPolls) await waitForDeviceLoginPoll(pollMs, deps);
752
+ }
753
+
754
+ finalStatus = {
755
+ ...finalStatus,
756
+ status: 'timeout',
757
+ error: `device login did not complete within ${Math.round(timeoutMs / 60000)} minutes`,
758
+ };
759
+ if (options.setup) {
760
+ console.error(`could not connect the ${provider} account; please try again.`);
761
+ } else {
762
+ printDeviceLoginFinalJson(finalStatus, options);
763
+ console.error('engine login device flow timed out');
764
+ }
765
+ return 1;
766
+ }
767
+
768
+ async function runEngineLoginCommand(args, root, deps = {}) {
769
+ const options = parseEngineLoginArgs(args);
770
+ if (options.help) {
771
+ printEngineLoginHelp();
772
+ return 0;
773
+ }
774
+
775
+ if (options.list) {
776
+ const result = await listEngineLogins(deps);
777
+ if (!result.ok) {
778
+ console.error(result.authError ? 'Run atris login first.' : `engine login list failed: ${result.error || result.status}`);
779
+ return 1;
780
+ }
781
+ printBackendResult(result, { json: options.json });
782
+ return 0;
783
+ }
784
+
785
+ if (options.remove) {
786
+ const provider = normalizeLoginProvider(options.remove);
787
+ if (!provider) {
788
+ console.error(`Unknown engine login provider "${options.remove}". Known providers: ${knownLoginProviders().join(', ')}`);
789
+ return 2;
790
+ }
791
+ const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}`, {
792
+ method: 'DELETE',
793
+ timeoutMs: 15000,
794
+ retries: 0,
795
+ }, deps);
796
+ if (!result.ok) {
797
+ console.error(result.authError ? 'Run atris login first.' : `engine login remove failed: ${result.error || result.status}`);
798
+ return 1;
799
+ }
800
+ printBackendResult(result, { json: options.json });
801
+ return 0;
802
+ }
803
+
804
+ const provider = normalizeLoginProvider(options.provider);
805
+ if (!provider) {
806
+ console.error(`usage: atris engine login <provider> --yes; providers: ${knownLoginProviders().join(', ')}`);
807
+ return 2;
808
+ }
809
+
810
+ if (options.seatFlag && !options.computer) {
811
+ console.error('use --seat with --computer or --business');
812
+ return 2;
813
+ }
814
+
815
+ if (options.computer) {
816
+ return runEngineDeviceLoginCommand(provider, options, deps);
817
+ }
818
+
819
+ let built;
820
+ try {
821
+ built = await buildEngineLoginPayload(provider, deps);
822
+ } catch (err) {
823
+ console.error(err.message);
824
+ return err.code === 'missing_file' || err.code === 'missing_api_key' ? 1 : 2;
825
+ }
826
+
827
+ printLoginSummary(provider, built.summary);
828
+ if (!options.yes) {
829
+ const confirmed = await confirmEngineLoginUpload(provider, deps);
830
+ if (!confirmed) {
831
+ console.error('engine login upload cancelled');
832
+ return 1;
833
+ }
834
+ }
835
+
836
+ const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}`, {
837
+ method: 'POST',
838
+ body: built.payload,
839
+ timeoutMs: 30000,
840
+ retries: 0,
841
+ }, deps);
842
+ if (!result.ok) {
843
+ console.error(result.authError ? 'Run atris login first.' : `engine login upload failed: ${result.error || result.status}`);
844
+ return 1;
845
+ }
846
+ printBackendResult(result, { json: options.json });
847
+ return 0;
848
+ }
849
+
850
+ async function runEngineSeedCommand(args, root, deps = {}) {
851
+ const options = parseEngineSeedArgs(args);
852
+ if (options.help) {
853
+ printEngineSeedHelp();
854
+ return 0;
855
+ }
856
+
857
+ const provider = normalizeLoginProvider(options.provider);
858
+ if (!provider) {
859
+ console.error(`usage: atris engine seed <provider> --business <id>|--user; providers: ${knownLoginProviders().join(', ')}`);
860
+ return 2;
861
+ }
862
+ if ((options.business && options.user) || (!options.business && !options.user)) {
863
+ console.error('usage: atris engine seed <provider> --business <id>|--user');
864
+ return 2;
865
+ }
866
+
867
+ const body = options.user
868
+ ? { target: { type: 'user' } }
869
+ : { target: { type: 'business', id: options.business } };
870
+ const result = await authenticatedEngineApi(`/engines/logins/${encodeURIComponent(provider)}/seed`, {
871
+ method: 'POST',
872
+ body,
873
+ timeoutMs: 60000,
874
+ retries: 0,
875
+ }, deps);
876
+ if (!result.ok) {
877
+ console.error(result.authError ? 'Run atris login first.' : `engine seed failed: ${result.error || result.status}`);
878
+ return 1;
879
+ }
880
+ printBackendResult(result, { json: options.json });
881
+ return 0;
882
+ }
883
+
96
884
  function printRoster(root) {
97
885
  const list = roster(root);
98
886
  const found = list.filter((e) => e.installed).length;
@@ -102,8 +890,9 @@ function printRoster(root) {
102
890
  console.log('');
103
891
  for (const engine of list) {
104
892
  const mark = engine.default ? '→' : ' ';
105
- const state = engine.installed ? 'ready' : 'not installed';
106
- console.log(` ${mark} ${engine.name.padEnd(12)} ${state}`);
893
+ const state = engine.health.status === 'not_installed' ? 'not installed' : engine.health.status.replace(/_/g, ' ');
894
+ const roles = engine.roles.join(',');
895
+ console.log(` ${mark} ${engine.id.padEnd(12)} ${state.padEnd(13)} ${engine.tier.padEnd(4)} ${roles}`);
107
896
  }
108
897
  console.log('');
109
898
  console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
@@ -111,6 +900,80 @@ function printRoster(root) {
111
900
  console.log('');
112
901
  }
113
902
 
903
+ function registryPayload(root) {
904
+ const current = resolveDefaultEngine(root);
905
+ const registry = readEngineRegistry(root);
906
+ return {
907
+ default: current.name,
908
+ source: current.source,
909
+ engines: registry.engines.map((engine) => ({
910
+ ...engine,
911
+ default: engine.id === current.name,
912
+ })),
913
+ };
914
+ }
915
+
916
+ function parseSetFlag(args) {
917
+ const prefix = '--set=';
918
+ for (let i = 0; i < args.length; i += 1) {
919
+ const arg = String(args[i]);
920
+ if (arg === '--set') return args[i + 1] || '';
921
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
922
+ }
923
+ return '';
924
+ }
925
+
926
+ function runResolveCommand(args, root) {
927
+ const json = args.includes('--json');
928
+ const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
929
+ if (!role) {
930
+ const message = `usage: atris engine resolve <role>; roles: ${ENGINE_ROLES.join(', ')}`;
931
+ if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
932
+ else console.error(message);
933
+ return 2;
934
+ }
935
+ let engine;
936
+ try {
937
+ engine = resolveEngineForRole(role, root);
938
+ } catch (err) {
939
+ if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
940
+ else console.error(err.message);
941
+ return 2;
942
+ }
943
+ if (!engine) {
944
+ const message = `No ready installed engine can fill role "${role}".`;
945
+ if (json) console.log(JSON.stringify({ ok: false, role, error: message }, null, 2));
946
+ else console.error(message);
947
+ return 1;
948
+ }
949
+ if (json) console.log(JSON.stringify(engine, null, 2));
950
+ else console.log(engine.id);
951
+ return 0;
952
+ }
953
+
954
+ function runHealthCommand(args, root) {
955
+ const json = args.includes('--json');
956
+ const positional = args.filter((a) => !String(a).startsWith('--'));
957
+ const name = positional[0] || '';
958
+ const status = parseSetFlag(args);
959
+ if (!name || !status) {
960
+ const message = `usage: atris engine health <name> --set <status>; statuses: ${ENGINE_HEALTH_STATUSES.join(', ')}`;
961
+ if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
962
+ else console.error(message);
963
+ return 2;
964
+ }
965
+ try {
966
+ const engine = setEngineHealth(name, status, root);
967
+ if (json) console.log(JSON.stringify(engine, null, 2));
968
+ else console.log(`engine ${engine.id} health: ${engine.health.status}`);
969
+ return 0;
970
+ } catch (err) {
971
+ if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
972
+ else console.error(err.message);
973
+ return 2;
974
+ }
975
+ }
976
+
114
977
  // Preflight: run one engine CLI headless with a reply-OK prompt and report
115
978
  // pass/fail. A dead login, missing binary, or hung spawn is a one-command
116
979
  // diagnosis instead of a failed overnight flight. `name` is canonical.
@@ -189,6 +1052,37 @@ function probeEngine(name, { timeout = PROBE_DEFAULT_TIMEOUT_MS } = {}) {
189
1052
  };
190
1053
  }
191
1054
 
1055
+ // A probe is a real engine round trip on the same command shape dispatches
1056
+ // use, so its pass/fail and latency are routing evidence. Record one receipt
1057
+ // per role the engine serves so the router brain (lib/router-brain.js) can
1058
+ // learn from every preflight. Liveness evidence only: real build outcomes
1059
+ // land in the same pool via task receipts and outweigh probes over time.
1060
+ function writeProbeReceipt(result, root = process.cwd()) {
1061
+ const runsDir = path.join(root, 'atris', 'runs');
1062
+ fs.mkdirSync(runsDir, { recursive: true });
1063
+ let roles = [];
1064
+ try {
1065
+ const engine = engineRegistryView(root).find((row) => row.id === result.engine);
1066
+ roles = engine && Array.isArray(engine.roles) ? engine.roles : [];
1067
+ } catch { /* registry unavailable: still record the probe */ }
1068
+ if (!roles.length) roles = ['executor'];
1069
+ const at = new Date().toISOString();
1070
+ const stampMs = Date.now();
1071
+ for (const role of roles) {
1072
+ const receipt = {
1073
+ schema: 'atris.engine_probe_receipt.v1',
1074
+ engine: result.engine,
1075
+ task_type: role,
1076
+ verified_passed: Boolean(result.pass),
1077
+ duration_ms: result.durationMs,
1078
+ reason: result.reason,
1079
+ at,
1080
+ };
1081
+ const file = path.join(runsDir, `engine-probe-task-${result.engine}-${role}-${stampMs}.json`);
1082
+ fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
1083
+ }
1084
+ }
1085
+
192
1086
  function runEngineTest(targets, { json, root } = {}) {
193
1087
  let enginesToTest;
194
1088
  if (targets && targets.length) {
@@ -213,6 +1107,14 @@ function runEngineTest(targets, { json, root } = {}) {
213
1107
  const results = enginesToTest.map((name) => probeEngine(name));
214
1108
  const failures = results.filter((r) => !r.pass);
215
1109
  const passed = results.length - failures.length;
1110
+ // A live probe is the freshest health evidence there is: write it back so
1111
+ // a transient failure does not leave an engine stuck on error forever.
1112
+ for (const r of results) {
1113
+ try { setEngineHealth(r.engine, r.pass ? 'ready' : 'error', root); } catch { /* best-effort */ }
1114
+ if (r.reason !== 'not-installed') {
1115
+ try { writeProbeReceipt(r, root); } catch { /* best-effort */ }
1116
+ }
1117
+ }
216
1118
  if (json) {
217
1119
  console.log(JSON.stringify({
218
1120
  ok: failures.length === 0,
@@ -253,6 +1155,7 @@ function parseDispatchArgs(args) {
253
1155
  let promptFile = '';
254
1156
  let base = '';
255
1157
  let json = false;
1158
+ let yolo = false;
256
1159
  for (let i = 0; i < args.length; i += 1) {
257
1160
  const a = args[i];
258
1161
  if (a === '--engine') { engine = args[i + 1] || ''; i += 1; continue; }
@@ -262,16 +1165,17 @@ function parseDispatchArgs(args) {
262
1165
  if (a === '--base') { base = args[i + 1] || ''; i += 1; continue; }
263
1166
  if (a.startsWith('--base=')) { base = a.slice('--base='.length); continue; }
264
1167
  if (a === '--json') { json = true; continue; }
1168
+ if (a === '--yolo') { yolo = true; continue; }
265
1169
  if (a.startsWith('--')) continue;
266
1170
  taskIds.push(a);
267
1171
  }
268
- return { taskIds, engine, promptFile, base, json };
1172
+ return { taskIds, engine, promptFile, base, json, yolo };
269
1173
  }
270
1174
 
271
1175
  function runDispatchCommand(args, root) {
272
- const { taskIds, engine, promptFile, base, json } = parseDispatchArgs(args);
1176
+ const { taskIds, engine, promptFile, base, json, yolo } = parseDispatchArgs(args);
273
1177
  if (!taskIds.length || !engine) {
274
- console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]');
1178
+ console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]');
275
1179
  return 2;
276
1180
  }
277
1181
  const canonical = canonicalEngineName(engine);
@@ -299,13 +1203,13 @@ function runDispatchCommand(args, root) {
299
1203
  console.error(`engine dispatch: ${canonical} CLI (${def.bin}) is not installed here`);
300
1204
  return 2;
301
1205
  }
302
- return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
1206
+ return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, yolo, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
303
1207
  if (json) console.log(JSON.stringify(flight, null, 2));
304
1208
  return flight.paused.length ? 1 : 0;
305
1209
  });
306
1210
  }
307
1211
 
308
- function engineCommand(args = []) {
1212
+ function engineCommand(args = [], deps = {}) {
309
1213
  const root = process.cwd();
310
1214
  if ((args[0] || '').trim() === 'dispatch') {
311
1215
  return runDispatchCommand(args.slice(1), root);
@@ -315,14 +1219,33 @@ function engineCommand(args = []) {
315
1219
  const positional = args.filter((a) => !a.startsWith('--'));
316
1220
  const sub = (positional[0] || '').trim();
317
1221
 
1222
+ if (sub === 'login') {
1223
+ return runEngineLoginCommand(args.slice(args.indexOf('login') + 1), root);
1224
+ }
1225
+
1226
+ if (sub === 'seed') {
1227
+ return runEngineSeedCommand(args.slice(args.indexOf('seed') + 1), root);
1228
+ }
1229
+
1230
+ if (sub === 'seats') {
1231
+ return runEngineSeatsCommand(deps);
1232
+ }
1233
+
318
1234
  if (sub === 'test') {
319
1235
  return runEngineTest(positional.slice(1), { json, root });
320
1236
  }
321
1237
 
1238
+ if (sub === 'resolve') {
1239
+ return runResolveCommand(args.slice(args.indexOf('resolve') + 1), root);
1240
+ }
1241
+
1242
+ if (sub === 'health') {
1243
+ return runHealthCommand(args.slice(args.indexOf('health') + 1), root);
1244
+ }
1245
+
322
1246
  if (!sub || sub === 'list' || sub === 'status') {
323
1247
  if (json) {
324
- const current = resolveDefaultEngine(root);
325
- console.log(JSON.stringify({ engines: roster(root), default: current.name, source: current.source }, null, 2));
1248
+ console.log(JSON.stringify(registryPayload(root), null, 2));
326
1249
  return 0;
327
1250
  }
328
1251
  printRoster(root);
@@ -338,11 +1261,23 @@ function engineCommand(args = []) {
338
1261
  }
339
1262
 
340
1263
  if (sub === 'help') {
341
- console.log('\n atris engine roster + current default\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 dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>]\n one-command claim, worktree, build, verify, ship, ready\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
1264
+ console.log('\n atris engine roster + current default\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\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 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');
342
1265
  return 0;
343
1266
  }
344
1267
 
345
- // atris engine <name> — flip the default.
1268
+ // atris engine <name> — flip the default. An unknown name is a plain user
1269
+ // typo, not a crash: report it cleanly (and as JSON on demand) instead of
1270
+ // letting setEngine throw a raw stack trace at a new person.
1271
+ if (!canonicalEngineName(sub)) {
1272
+ const known = RUNNER_PROFILE_NAMES.join(', ');
1273
+ if (json) {
1274
+ console.log(JSON.stringify({ ok: false, error: `Unknown engine "${sub}"`, known: RUNNER_PROFILE_NAMES }, null, 2));
1275
+ } else {
1276
+ console.error(`\n Unknown engine "${sub}". known engines: ${known}`);
1277
+ console.error(' run "atris engine" to see the roster, or "atris engine help".\n');
1278
+ }
1279
+ return 2;
1280
+ }
346
1281
  const canonical = setEngine(sub, root);
347
1282
  const def = RUNNER_PROFILE_DEFS[canonical];
348
1283
  const installed = binInstalled(def.bin);
@@ -362,9 +1297,35 @@ module.exports = {
362
1297
  setEngine,
363
1298
  resetEngine,
364
1299
  roster,
1300
+ registryPayload,
1301
+ engineRegistryFile,
1302
+ readEngineRegistry,
1303
+ resolveEngineForRole,
1304
+ setEngineHealth,
365
1305
  probeEngine,
366
1306
  runEngineTest,
367
1307
  parseDispatchArgs,
368
1308
  runDispatchCommand,
1309
+ ENGINE_LOGIN_MANIFESTS,
1310
+ MAX_ENGINE_LOGIN_FILE_BYTES,
1311
+ expandHomePath,
1312
+ normalizeLoginProvider,
1313
+ normalizeEngineLoginSeat,
1314
+ validEngineLoginSeat,
1315
+ engineLoginSeatError,
1316
+ detectedEmailFromContent,
1317
+ readEngineLoginFiles,
1318
+ parseEngineLoginArgs,
1319
+ parseEngineSeedArgs,
1320
+ redactBackendResponse,
1321
+ buildEngineLoginPayload,
1322
+ listEngineLogins,
1323
+ listEngineSeats,
1324
+ readyCheckEngineLogin,
1325
+ formatEngineSeats,
1326
+ runEngineSeatsCommand,
1327
+ runEngineDeviceLoginCommand,
1328
+ runEngineLoginCommand,
1329
+ runEngineSeedCommand,
369
1330
  HOUSE_ENGINE,
370
1331
  };