atris 3.53.0 → 3.56.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 (119) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/x-search/SKILL.md +24 -22
  7. package/atris/skills/youtube/SKILL.md +82 -33
  8. package/atris/team/_template/SOUL.md +40 -0
  9. package/atris/team/validator/MEMBER.md +1 -2
  10. package/bin/atris +21 -0
  11. package/bin/atris.js +563 -373
  12. package/commands/api-key.js +170 -0
  13. package/commands/auth.js +453 -43
  14. package/commands/autoland.js +3 -1
  15. package/commands/autopilot-front.js +58 -11
  16. package/commands/avail.js +5 -4
  17. package/commands/balance.js +55 -0
  18. package/commands/bench.js +29 -1
  19. package/commands/brain.js +63 -12
  20. package/commands/brainstorm.js +18 -0
  21. package/commands/business.js +149 -1
  22. package/commands/chat-scan.js +17 -4
  23. package/commands/close.js +5 -1
  24. package/commands/doctor.js +174 -0
  25. package/commands/dream.js +7 -0
  26. package/commands/drill.js +34 -2
  27. package/commands/engine.js +47 -35
  28. package/commands/errors.js +58 -8
  29. package/commands/experiments.js +14 -0
  30. package/commands/feedback.js +61 -2
  31. package/commands/fleet-report.js +27 -7
  32. package/commands/fleet.js +98 -11
  33. package/commands/founder.js +60 -21
  34. package/commands/human-missions.js +79 -4
  35. package/commands/improve.js +54 -4
  36. package/commands/init.js +68 -92
  37. package/commands/install.js +40 -0
  38. package/commands/integrations.js +565 -30
  39. package/commands/interview.js +8 -0
  40. package/commands/land.js +48 -17
  41. package/commands/learn.js +29 -4
  42. package/commands/log.js +51 -0
  43. package/commands/member.js +68 -10
  44. package/commands/mission.js +287 -50
  45. package/commands/now.js +15 -3
  46. package/commands/orb.js +97 -2
  47. package/commands/pack.js +83 -9
  48. package/commands/playbook.js +381 -0
  49. package/commands/plugin.js +16 -0
  50. package/commands/radar.js +31 -7
  51. package/commands/recap.js +128 -21
  52. package/commands/release.js +102 -2
  53. package/commands/review.js +5 -10
  54. package/commands/revisions.js +55 -0
  55. package/commands/run-front.js +12 -4
  56. package/commands/scout.js +12 -1
  57. package/commands/search.js +123 -28
  58. package/commands/sign.js +2 -2
  59. package/commands/signup.js +12 -3
  60. package/commands/site-deploy.js +17 -4
  61. package/commands/site.js +2 -2
  62. package/commands/skill-eval.js +289 -0
  63. package/commands/skill.js +56 -6
  64. package/commands/social.js +603 -0
  65. package/commands/spaceship.js +69 -6
  66. package/commands/stream.js +8 -1
  67. package/commands/study.js +32 -19
  68. package/commands/sync.js +28 -22
  69. package/commands/task.js +435 -60
  70. package/commands/teach.js +72 -63
  71. package/commands/terminal.js +25 -9
  72. package/commands/topup.js +88 -0
  73. package/commands/truth.js +52 -6
  74. package/commands/usage.js +91 -0
  75. package/commands/verify.js +3 -2
  76. package/commands/version.js +3 -1
  77. package/commands/voice.js +5 -1
  78. package/commands/who.js +69 -11
  79. package/commands/wiki.js +7 -4
  80. package/commands/wish.js +76 -5
  81. package/commands/workflow.js +327 -145
  82. package/commands/worktree.js +1 -0
  83. package/commands/write.js +5 -0
  84. package/commands/x-search.js +340 -0
  85. package/commands/xp.js +5 -1
  86. package/commands/youtube.js +551 -25
  87. package/lib/account-bound.js +47 -0
  88. package/lib/auto-accept-certified.js +8 -1
  89. package/lib/auto-lessons.js +186 -0
  90. package/lib/autoland.js +4 -2
  91. package/lib/chat-log-scan.js +7 -4
  92. package/lib/cli-json.js +77 -0
  93. package/lib/cli-scope.js +26 -0
  94. package/lib/codex-flight.js +1 -0
  95. package/lib/conductor-artifacts.js +1 -1
  96. package/lib/context-gatherer.js +11 -0
  97. package/lib/developer-api.js +116 -0
  98. package/lib/feedback-triage.js +274 -0
  99. package/lib/first-minute.js +398 -0
  100. package/lib/fleet.js +115 -34
  101. package/lib/functional-owner.js +22 -0
  102. package/lib/known-commands.js +31 -5
  103. package/lib/member-scaffold.js +197 -0
  104. package/lib/mission-root.js +4 -2
  105. package/lib/noninteractive.js +88 -0
  106. package/lib/policy-lessons.js +4 -1
  107. package/lib/revision-metric.js +279 -0
  108. package/lib/scratch-root.js +48 -0
  109. package/lib/skill-eval-gate.js +249 -0
  110. package/lib/task-db.js +4 -0
  111. package/lib/task-proof.js +69 -8
  112. package/lib/verifier-quality.js +69 -0
  113. package/lib/wish-audit.js +2 -2
  114. package/lib/wish-delegate.js +10 -1
  115. package/lib/workspace-scaffold.js +270 -0
  116. package/package.json +3 -2
  117. package/scripts/det/checklist-score.js +191 -0
  118. package/scripts/det/ytsearch +31 -0
  119. package/utils/update-check.js +15 -0
package/commands/auth.js CHANGED
@@ -1,22 +1,361 @@
1
- const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
2
2
  const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
3
+ const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
+ const { hasFlag, readFlag } = require('../lib/arg-parser');
3
5
  const fs = require('fs');
4
6
  const path = require('path');
5
7
 
8
+ const AGENT_TOKEN_PATH = '/auth/agent-token';
9
+ const DEFAULT_AGENT_SCOPES = ['x-search', 'youtube'];
10
+ const DEFAULT_DAILY_CREDIT_CAP = 50;
11
+ const NO_STORED_JWT_MESSAGE = 'not signed in. run atris login first.';
12
+
13
+ function firstNonEmptyString(...values) {
14
+ for (const value of values) {
15
+ if (typeof value === 'string' && value.trim()) return value.trim();
16
+ }
17
+ return null;
18
+ }
19
+
20
+ function wantsAgentToken(args = []) {
21
+ if (hasFlag(args, '--agent')) return true;
22
+ const first = args.find((arg) => arg && !String(arg).startsWith('-'));
23
+ return first === 'agent-token';
24
+ }
25
+
26
+ function parseScopeList(raw) {
27
+ if (!raw) return [...DEFAULT_AGENT_SCOPES];
28
+ const scopes = String(raw).split(',').map((part) => part.trim()).filter(Boolean);
29
+ if (scopes.length === 0) {
30
+ throw new Error('scopes must list at least one value');
31
+ }
32
+ return scopes;
33
+ }
34
+
35
+ function parseDailyCreditCap(args) {
36
+ const present = args.some((arg) => arg === '--daily-credit-cap' || String(arg).startsWith('--daily-credit-cap='));
37
+ const raw = readFlag(args, '--daily-credit-cap', '');
38
+ if (!present) return DEFAULT_DAILY_CREDIT_CAP;
39
+ const value = Number.parseInt(raw, 10);
40
+ if (!Number.isInteger(value) || value <= 0) {
41
+ throw new Error('daily credit cap must be a positive integer');
42
+ }
43
+ return value;
44
+ }
45
+
46
+ function parseAgentTokenArgs(args = []) {
47
+ return {
48
+ agent: wantsAgentToken(args),
49
+ scopes: parseScopeList(readFlag(args, '--scopes', '')),
50
+ dailyCreditCap: parseDailyCreditCap(args),
51
+ json: wantsJson(args),
52
+ help: hasFlag(args, '--help') || hasFlag(args, '-h') || args[0] === 'help',
53
+ };
54
+ }
55
+
56
+ function extractAgentAccessToken(data) {
57
+ if (!data || typeof data !== 'object') return null;
58
+ return firstNonEmptyString(
59
+ data.access_token,
60
+ data.token,
61
+ data.agent_access,
62
+ data.agent_token,
63
+ data.agent_access_token,
64
+ );
65
+ }
66
+
67
+ function expiryFromValue(value) {
68
+ if (typeof value === 'string' && value.trim()) return value.trim();
69
+ if (typeof value === 'number' && Number.isFinite(value)) {
70
+ const ms = value > 1e12 ? value : value * 1000;
71
+ return new Date(ms).toISOString();
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function extractAgentTokenMeta(data, requested, token) {
77
+ const payload = data && typeof data === 'object' ? data : {};
78
+ const scopes = Array.isArray(payload.scopes) && payload.scopes.length
79
+ ? payload.scopes.map(String)
80
+ : requested.scopes;
81
+ const rawCap = payload.daily_credit_cap;
82
+ const dailyCreditCap = Number.isFinite(Number(rawCap)) ? Number(rawCap) : requested.dailyCreditCap;
83
+ const expiresAt = expiryFromValue(payload.expires_at ?? payload.expiry ?? payload.expires ?? payload.exp)
84
+ || expiryFromValue(getTokenExpiryEpochSeconds(token));
85
+ return { scopes, dailyCreditCap, expiresAt };
86
+ }
87
+
88
+ function persistMintedAgentToken(credentials, token, extras = {}) {
89
+ const next = {
90
+ token,
91
+ refresh_token: extras.refresh_token || credentials.refresh_token || null,
92
+ email: credentials.email || null,
93
+ user_id: credentials.user_id || null,
94
+ provider: credentials.provider || null,
95
+ saved_at: extras.saved_at || new Date().toISOString(),
96
+ };
97
+ if (credentials.source_profile) {
98
+ saveProfile(credentials.source_profile, next);
99
+ return next;
100
+ }
101
+ saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider);
102
+ return next;
103
+ }
104
+
105
+ function printAgentTokenMint(meta, output) {
106
+ output('minted scoped agent token');
107
+ output(`scopes: ${meta.scopes.join(', ')}`);
108
+ output(`daily credit cap: ${meta.dailyCreditCap}`);
109
+ if (meta.expiresAt) output(`expires: ${meta.expiresAt}`);
110
+ }
111
+
112
+ function redactSecret(text, secret) {
113
+ if (!secret || !text) return text;
114
+ return String(text).split(secret).join('[redacted]');
115
+ }
116
+
117
+ async function postAgentToken(api, token, body) {
118
+ return api(AGENT_TOKEN_PATH, {
119
+ method: 'POST',
120
+ token,
121
+ body,
122
+ retries: 0,
123
+ });
124
+ }
125
+
126
+ async function mintScopedAgentToken(requested = {}, deps = {}) {
127
+ const api = deps.apiRequestJson || apiRequestJson;
128
+ const load = deps.loadCredentials || loadCredentials;
129
+ const persist = deps.persistMintedAgentToken || persistMintedAgentToken;
130
+ const now = deps.now || (() => new Date().toISOString());
131
+
132
+ const scopes = Array.isArray(requested.scopes)
133
+ ? requested.scopes.map((part) => String(part).trim()).filter(Boolean)
134
+ : parseScopeList(requested.scopes);
135
+ if (scopes.length === 0) {
136
+ return { ok: false, code: 'invalid_scopes', error: 'scopes must list at least one value' };
137
+ }
138
+ const dailyCreditCap = Number.isInteger(requested.dailyCreditCap) && requested.dailyCreditCap > 0
139
+ ? requested.dailyCreditCap
140
+ : DEFAULT_DAILY_CREDIT_CAP;
141
+
142
+ const credentials = load() || {};
143
+ const accessToken = firstNonEmptyString(credentials.token);
144
+ const refreshToken = firstNonEmptyString(credentials.refresh_token);
145
+ if (!accessToken && !refreshToken) {
146
+ return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
147
+ }
148
+
149
+ const body = {
150
+ scopes,
151
+ daily_credit_cap: dailyCreditCap,
152
+ };
153
+ let authToken = accessToken || refreshToken;
154
+ let result = await postAgentToken(api, authToken, body);
155
+ if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
156
+ authToken = refreshToken;
157
+ result = await postAgentToken(api, authToken, body);
158
+ }
159
+
160
+ if (!result.ok) {
161
+ const detail = redactSecret(result.error || 'agent token request failed', accessToken);
162
+ return {
163
+ ok: false,
164
+ code: 'mint_failed',
165
+ status: result.status || 0,
166
+ error: redactSecret(detail, refreshToken),
167
+ };
168
+ }
169
+
170
+ const minted = extractAgentAccessToken(result.data);
171
+ if (!minted) {
172
+ return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
173
+ }
174
+
175
+ persist(credentials, minted, {
176
+ refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
177
+ saved_at: now(),
178
+ });
179
+
180
+ return {
181
+ ok: true,
182
+ token: minted,
183
+ meta: extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted),
184
+ };
185
+ }
186
+
187
+ async function ensureBilledCommandAuth(scope, deps = {}) {
188
+ const wanted = String(scope || '').trim();
189
+ if (!wanted) {
190
+ return { ok: false, error: NO_STORED_JWT_MESSAGE };
191
+ }
192
+
193
+ const api = deps.apiRequestJson || apiRequestJson;
194
+ const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
195
+ const load = deps.loadCredentials || loadCredentials;
196
+ const mint = deps.mintScopedAgentToken || mintScopedAgentToken;
197
+
198
+ if (!deps.forceMint) {
199
+ const ensured = await ensureFn(api);
200
+ if (ensured && ensured.credentials && ensured.credentials.token) {
201
+ return {
202
+ ok: true,
203
+ token: ensured.credentials.token,
204
+ minted: false,
205
+ credentials: ensured.credentials,
206
+ };
207
+ }
208
+ }
209
+
210
+ const credentials = load() || {};
211
+ if (!firstNonEmptyString(credentials.token, credentials.refresh_token)) {
212
+ return { ok: false, error: NO_STORED_JWT_MESSAGE };
213
+ }
214
+
215
+ const minted = await mint({ scopes: [wanted] }, deps);
216
+ if (!minted.ok) {
217
+ return { ok: false, error: minted.error || NO_STORED_JWT_MESSAGE };
218
+ }
219
+ return {
220
+ ok: true,
221
+ token: minted.token,
222
+ minted: true,
223
+ credentials: load() || credentials,
224
+ meta: minted.meta,
225
+ };
226
+ }
227
+
228
+ async function mintAgentToken(args = [], deps = {}) {
229
+ const output = deps.output || console.log;
230
+ const outputError = deps.outputError || console.error;
231
+
232
+ let options;
233
+ try {
234
+ options = parseAgentTokenArgs(args);
235
+ } catch (error) {
236
+ const message = error.message || String(error);
237
+ if (wantsJson(args)) {
238
+ output(JSON.stringify({ ok: false, error: message }, null, 2));
239
+ } else {
240
+ outputError(message);
241
+ }
242
+ return 1;
243
+ }
244
+
245
+ const minted = await mintScopedAgentToken({
246
+ scopes: options.scopes,
247
+ dailyCreditCap: options.dailyCreditCap,
248
+ }, deps);
249
+
250
+ if (!minted.ok) {
251
+ if (options.json) {
252
+ output(JSON.stringify({
253
+ ok: false,
254
+ error: minted.code === 'not_logged_in' ? 'not_logged_in' : minted.error,
255
+ next: minted.code === 'not_logged_in' ? 'atris login' : undefined,
256
+ status: minted.status || undefined,
257
+ }, null, 2));
258
+ } else if (minted.code === 'not_logged_in') {
259
+ output('not signed in. run atris login first, then atris login --agent.');
260
+ } else {
261
+ outputError(minted.error);
262
+ }
263
+ return 1;
264
+ }
265
+
266
+ if (options.json) {
267
+ output(JSON.stringify({
268
+ ok: true,
269
+ minted: true,
270
+ scopes: minted.meta.scopes,
271
+ daily_credit_cap: minted.meta.dailyCreditCap,
272
+ expires_at: minted.meta.expiresAt,
273
+ }, null, 2));
274
+ return 0;
275
+ }
276
+
277
+ printAgentTokenMint(minted.meta, output);
278
+ return 0;
279
+ }
280
+
281
+ async function printWhoamiPayload(asJson) {
282
+ const ensured = await ensureValidCredentials(apiRequestJson);
283
+ if (ensured.error) {
284
+ if (asJson) {
285
+ console.log(JSON.stringify({
286
+ ok: false,
287
+ logged_in: false,
288
+ error: ensured.error,
289
+ detail: ensured.detail || null,
290
+ }, null, 2));
291
+ } else {
292
+ console.log('Status: Not logged in');
293
+ if (ensured.detail) console.log(`Reason: ${ensured.detail}`);
294
+ console.log('\nRun "atris login" to sign in.');
295
+ }
296
+ process.exit(1);
297
+ }
298
+
299
+ const { credentials, user } = ensured;
300
+ const email = user?.email || credentials?.email || 'unknown';
301
+ const userId = user?.id || credentials?.user_id || 'unknown';
302
+ const provider = user?.provider || credentials?.provider || 'unknown';
303
+ const savedAt = credentials?.saved_at || 'unknown';
304
+ const payload = {
305
+ ok: true,
306
+ logged_in: true,
307
+ email,
308
+ user_id: userId,
309
+ provider,
310
+ credentials_saved: savedAt,
311
+ };
312
+
313
+ if (asJson) {
314
+ console.log(JSON.stringify(payload, null, 2));
315
+ process.exit(0);
316
+ }
317
+
318
+ const summary = await displayAccountSummary(apiRequestJson);
319
+ if (summary.error) {
320
+ console.log('\nRun "atris login" to sign in.');
321
+ process.exit(1);
322
+ }
323
+ process.exit(0);
324
+ }
325
+
6
326
  async function loginAtris(options = {}) {
7
327
  // Support: atris login --token <token> --force
8
328
  const args = process.argv.slice(3);
9
329
  const forceFlag = args.includes('--force') || args.includes('-f') || options.force;
10
330
  const tokenIndex = args.indexOf('--token');
11
331
  const directToken = tokenIndex !== -1 ? args[tokenIndex + 1] : options.token;
332
+ const asJson = wantsJson(args);
333
+ const nonInteractive = isNonInteractive(args);
12
334
 
13
335
  try {
14
336
  const existing = loadCredentials();
15
337
 
338
+ if (wantsAgentToken(args)) {
339
+ if (directToken) {
340
+ saveCredentials(
341
+ String(directToken).trim(),
342
+ existing?.refresh_token || null,
343
+ existing?.email || null,
344
+ existing?.user_id || null,
345
+ existing?.provider || 'manual',
346
+ );
347
+ }
348
+ const code = await mintAgentToken(args);
349
+ process.exit(code);
350
+ }
351
+
16
352
  // Direct token mode (non-interactive)
17
353
  if (directToken) {
18
354
  const trimmed = directToken.trim();
19
355
  saveCredentials(trimmed, null, existing?.email || null, existing?.user_id || null, existing?.provider || 'manual');
356
+ if (asJson) {
357
+ return printWhoamiPayload(true);
358
+ }
20
359
  console.log('Token saved. Validating…\n');
21
360
  const summary = await displayAccountSummary(apiRequestJson);
22
361
  if (summary.error) {
@@ -27,24 +366,48 @@ async function loginAtris(options = {}) {
27
366
  process.exit(0);
28
367
  }
29
368
 
369
+ // Already signed in without --force: behave like whoami (no menu).
370
+ // Print local identity without waiting on the network so headless agents
371
+ // never hang on a stale token validation.
30
372
  if (existing && !forceFlag) {
31
- const label = existing.email || existing.user_id || 'unknown';
32
- const profiles = listProfiles();
33
- console.log(`Currently signed in as: ${label}`);
34
- if (profiles.length > 1) {
35
- console.log(`${profiles.length} accounts saved. Use "atris switch" to change.\n`);
373
+ const email = existing.email || null;
374
+ const userId = existing.user_id || null;
375
+ const provider = existing.provider || null;
376
+ if (asJson) {
377
+ console.log(JSON.stringify({
378
+ ok: true,
379
+ logged_in: true,
380
+ email,
381
+ user_id: userId,
382
+ provider,
383
+ credentials_saved: existing.saved_at || null,
384
+ next: 'atris whoami --json',
385
+ }, null, 2));
386
+ process.exit(0);
36
387
  }
37
- console.log(' 1. Add another account');
38
- console.log(' 2. Re-login to current account');
39
- console.log(' 3. Cancel\n');
388
+ console.log(`Currently signed in as: ${email || userId || 'unknown'}`);
389
+ if (provider) console.log(`Provider: ${provider}`);
390
+ console.log('Next: atris whoami');
391
+ process.exit(0);
392
+ }
40
393
 
41
- const choice = await promptUser('Choice (1-3): ');
42
- if (choice === '3' || (!choice)) {
43
- console.log('Cancelled.');
44
- process.exit(0);
394
+ if (nonInteractive) {
395
+ if (asJson) {
396
+ console.log(JSON.stringify({
397
+ ok: false,
398
+ logged_in: false,
399
+ error: 'login requires --token in non-interactive mode',
400
+ next: 'atris login --token <token>',
401
+ }, null, 2));
402
+ } else {
403
+ console.log('Not signed in.');
404
+ console.log('login needs a terminal for browser OAuth, or pass a token.');
405
+ console.log('Next: atris login --token <token>');
45
406
  }
46
- // Both 1 and 2 proceed to OAuth — the difference is just the prompt
47
- } else if (!existing) {
407
+ process.exit(1);
408
+ }
409
+
410
+ if (!existing) {
48
411
  console.log('Welcome to Atris! Let\'s get you signed in.\n');
49
412
  }
50
413
 
@@ -154,17 +517,21 @@ function logoutAtris() {
154
517
  }
155
518
 
156
519
  async function whoamiAtris() {
157
- const { apiRequestJson } = require('../utils/api');
520
+ const args = process.argv.slice(3);
521
+ const asJson = wantsJson(args);
158
522
 
159
523
  try {
160
- const summary = await displayAccountSummary(apiRequestJson);
161
- if (summary.error) {
162
- console.log('\nRun "atris login" to sign in.');
163
- process.exit(1);
164
- }
165
- process.exit(0);
524
+ return await printWhoamiPayload(asJson);
166
525
  } catch (error) {
167
- console.error(`Failed to fetch account: ${error.message || error}`);
526
+ if (asJson) {
527
+ console.log(JSON.stringify({
528
+ ok: false,
529
+ logged_in: false,
530
+ error: error.message || String(error),
531
+ }, null, 2));
532
+ } else {
533
+ console.error(`Failed to fetch account: ${error.message || error}`);
534
+ }
168
535
  process.exit(1);
169
536
  }
170
537
  }
@@ -318,16 +685,18 @@ function useAccount() {
318
685
  }
319
686
  }
320
687
 
321
- async function accountsCmd() {
322
- const args = process.argv.slice(3);
323
- const subCmd = args[0];
688
+ async function accountsCmd(argv = null) {
689
+ const args = Array.isArray(argv) ? argv : process.argv.slice(3);
690
+ const { parseScopeFlag } = require('../lib/cli-scope');
691
+ const scope = parseScopeFlag(args);
692
+ const subCmd = scope.args[0];
324
693
 
325
694
  if (subCmd === 'add' || subCmd === 'login') {
326
695
  return loginAtris({ force: true });
327
696
  }
328
697
 
329
698
  if (subCmd === 'remove' || subCmd === 'rm') {
330
- const target = args[1];
699
+ const target = scope.args[1];
331
700
  if (target === '--all') {
332
701
  const profiles = listProfiles();
333
702
  if (profiles.length === 0) {
@@ -386,34 +755,59 @@ async function accountsCmd() {
386
755
  process.exit(0);
387
756
  }
388
757
 
389
- // Default: list accounts
390
- return listAccountsCmd();
758
+ // Default: list accounts (workspace = active only; --global = all profiles)
759
+ return listAccountsCmd({ global: scope.global, json: scope.args.includes('--json') || args.includes('--json') });
391
760
  }
392
761
 
393
- function listAccountsCmd() {
762
+ function listAccountsCmd(options = {}) {
394
763
  const profiles = listProfiles();
395
- if (profiles.length === 0) {
396
- console.log('No accounts saved. Run "atris login" to add one.');
397
- process.exit(0);
398
- }
399
-
400
764
  const current = loadCredentials();
401
765
  const currentUid = current?.user_id;
402
- // Also check which profile name is active (env var or session)
403
766
  const envProfile = process.env.ATRIS_PROFILE;
404
767
  const sessionProfile = getSessionProfile();
768
+ const scopeKind = options.global ? 'global' : 'workspace';
405
769
 
406
- console.log('\n Accounts\n');
407
- profiles.forEach(name => {
770
+ const rows = profiles.map((name) => {
408
771
  const profile = loadProfile(name);
409
772
  const email = profile?.email || 'unknown';
410
- const isActive = profile?.user_id === currentUid;
411
- if (isActive) {
412
- console.log(` ● ${name} ${email}`);
773
+ const isActive = profile?.user_id === currentUid
774
+ || name === envProfile
775
+ || name === sessionProfile;
776
+ return { name, email, active: Boolean(isActive) };
777
+ });
778
+
779
+ const visible = options.global ? rows : rows.filter((row) => row.active);
780
+
781
+ if (options.json) {
782
+ console.log(JSON.stringify({
783
+ scope: scopeKind,
784
+ accounts: visible,
785
+ total_profiles: profiles.length,
786
+ }, null, 2));
787
+ process.exit(0);
788
+ }
789
+
790
+ if (visible.length === 0) {
791
+ if (profiles.length === 0) {
792
+ console.log('No accounts saved. Run "atris login" to add one.');
413
793
  } else {
414
- console.log(` ${name} ${email}`);
794
+ console.log('No active account in this workspace.');
795
+ console.log('Pass --global to list all saved profiles, or atris switch <name>.');
796
+ }
797
+ process.exit(0);
798
+ }
799
+
800
+ console.log(`\n Accounts (${scopeKind})\n`);
801
+ visible.forEach((row) => {
802
+ if (row.active) {
803
+ console.log(` ● ${row.name} ${row.email}`);
804
+ } else {
805
+ console.log(` ${row.name} ${row.email}`);
415
806
  }
416
807
  });
808
+ if (!options.global && profiles.length > visible.length) {
809
+ console.log(`\n ${profiles.length - visible.length} more profile(s) hidden. Pass --global to list them.`);
810
+ }
417
811
  console.log(`\n Switch: atris switch <name>`);
418
812
  console.log(` Add: atris accounts add`);
419
813
  console.log(` Remove: atris accounts remove <name>\n`);
@@ -569,4 +963,20 @@ function shellInit() {
569
963
  console.log(lines.join('\n'));
570
964
  }
571
965
 
572
- module.exports = { loginAtris, logoutAtris, whoamiAtris, switchAccount, useAccount, accountsCmd, listAccountsCmd, resolveProfile, profileEmail, activateGlobal, switchSession, shellInit };
966
+ module.exports = {
967
+ loginAtris,
968
+ logoutAtris,
969
+ whoamiAtris,
970
+ switchAccount,
971
+ useAccount,
972
+ accountsCmd,
973
+ resolveProfile,
974
+ profileEmail,
975
+ activateGlobal,
976
+ switchSession,
977
+ shellInit,
978
+ parseAgentTokenArgs,
979
+ mintAgentToken,
980
+ ensureBilledCommandAuth,
981
+ wantsAgentToken,
982
+ };
@@ -282,8 +282,10 @@ function verifyClosedTaskMissions(root, tasks, { limit = MAX_MISSION_AUTO_VERIFY
282
282
  'tick',
283
283
  mission.id,
284
284
  '--verify',
285
+ '--yes',
286
+ '--full',
285
287
  '--summary',
286
- 'All linked repair tasks are closed; autoland re-ran the mission verifier.',
288
+ 'The linked repair tasks are closed, so the mission check ran again.',
287
289
  '--json',
288
290
  ]);
289
291
  const outcome = parseMissionVerifyResult(mission.id, result);
@@ -8,6 +8,7 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { spawn } = require('child_process');
10
10
  const { pickRunnableMission, runBudgetSeconds } = require('./run-front');
11
+ const { refuseHeadlessUnless, wantsJson, hasYesFlag } = require('../lib/noninteractive');
11
12
 
12
13
  const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
13
14
  const DEFAULT_LEG_WALL_SECONDS = 3600;
@@ -120,19 +121,40 @@ function legPlan(root, legWallSeconds) {
120
121
  };
121
122
  }
122
123
 
123
- function driveLeg(root, legArgs, current) {
124
+ function watchStopFile(root, child, { intervalMs = 2000 } = {}) {
125
+ const poll = setInterval(() => {
126
+ if (stopRequested(root)) {
127
+ try { child.kill('SIGTERM'); } catch {}
128
+ }
129
+ }, intervalMs);
130
+ if (poll.unref) poll.unref();
131
+ return () => clearInterval(poll);
132
+ }
133
+
134
+ function recordChildPid(root, state, pid) {
135
+ if (!state) return;
136
+ if (Number.isFinite(pid) && pid > 0) state.child_pid = pid;
137
+ else delete state.child_pid;
138
+ writeState(root, state);
139
+ }
140
+
141
+ function killPid(pid) {
142
+ if (!pidAlive(pid)) return false;
143
+ try { process.kill(pid, 'SIGTERM'); return true; } catch { return false; }
144
+ }
145
+
146
+ function driveLeg(root, legArgs, current, state) {
124
147
  return new Promise((resolve) => {
125
148
  const child = spawn(process.execPath, [CLI_PATH, ...legArgs], { cwd: root, stdio: 'inherit' });
126
149
  current.child = child;
150
+ recordChildPid(root, state, child.pid);
127
151
  // Stop fast: a stop file written by `atris autopilot stop` in another
128
152
  // terminal terminates the running leg, not just the loop between legs.
129
- const poll = setInterval(() => {
130
- if (stopRequested(root)) { try { child.kill('SIGTERM'); } catch {} }
131
- }, 2000);
132
- if (poll.unref) poll.unref();
153
+ const stopWatch = watchStopFile(root, child);
133
154
  const finish = (code) => {
134
- clearInterval(poll);
155
+ stopWatch();
135
156
  current.child = null;
157
+ recordChildPid(root, state, null);
136
158
  resolve(Number.isFinite(code) ? code : 1);
137
159
  };
138
160
  child.on('error', () => finish(1));
@@ -144,10 +166,14 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
144
166
 
145
167
  function autopilotStop(root = process.cwd()) {
146
168
  requestStop(root);
147
- const state = readState(root);
148
- if (state && pidAlive(state.pid)) {
149
- try { process.kill(state.pid, 'SIGTERM'); } catch {}
150
- console.log(`Stopping autopilot (pid ${state.pid}).`);
169
+ const state = readState(root) || {};
170
+ const wrapperPid = Number(state.pid);
171
+ const childPid = Number(state.child_pid);
172
+ const killedChild = killPid(childPid);
173
+ const killedWrapper = wrapperPid !== childPid && killPid(wrapperPid);
174
+ if (killedChild || killedWrapper) {
175
+ const extra = killedChild && Number.isFinite(childPid) ? `, leg ${childPid}` : '';
176
+ console.log(`Stopping autopilot (pid ${Number.isFinite(wrapperPid) ? wrapperPid : childPid}${extra}).`);
151
177
  } else {
152
178
  console.log('No live autopilot process; stop marker written so the next start clears it.');
153
179
  }
@@ -194,6 +220,26 @@ async function autopilotFront(args = []) {
194
220
  if (args[0] === 'stop') return autopilotStop(root);
195
221
  if (args[0] === 'status') return autopilotStatus(root);
196
222
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') { showFrontHelp(); return 0; }
223
+ if (wantsJson(args) && !hasYesFlag(args)) {
224
+ const state = readState(root);
225
+ const alive = Boolean(state && pidAlive(state.pid));
226
+ console.log(JSON.stringify({
227
+ ok: false,
228
+ command: 'autopilot',
229
+ running: alive,
230
+ error: 'pass --yes to start',
231
+ usage: 'atris autopilot [--yes|--auto] [--once]',
232
+ }, null, 2));
233
+ return 2;
234
+ }
235
+ // Bare headless invoke used to start a mission loop and hang. --auto is
236
+ // the existing proceed flag pulse and spaceship already pass. --once is
237
+ // a duration, not consent: it still needs --yes off a TTY.
238
+ if (!args.includes('--auto') && refuseHeadlessUnless(args, {
239
+ allowYes: true,
240
+ allowOnce: false,
241
+ usage: 'Usage: atris autopilot [--yes|--auto] [--once]',
242
+ })) return 2;
197
243
 
198
244
  const existing = readState(root);
199
245
  if (existing && pidAlive(existing.pid) && existing.pid !== process.pid) {
@@ -245,7 +291,7 @@ async function autopilotFront(args = []) {
245
291
  console.log(`\nautopilot leg ${state.legs}: ${plan.label}`);
246
292
 
247
293
  const legStarted = Date.now();
248
- const code = await driveLeg(root, plan.args, current);
294
+ const code = await driveLeg(root, plan.args, current, state);
249
295
  state.last_exit = code;
250
296
  writeState(root, state);
251
297
 
@@ -284,4 +330,5 @@ module.exports = {
284
330
  clearStop,
285
331
  readState,
286
332
  writeState,
333
+ watchStopFile,
287
334
  };