atris 3.57.4 → 3.58.6

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 (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +66 -21
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +59 -31
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +216 -50
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +101 -48
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +93 -22
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +1019 -123
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytnotes +82 -5
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +47 -9
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -0,0 +1,175 @@
1
+ /**
2
+ * atris ask: chat with your personal Atris computer.
3
+ *
4
+ * Same brain as texting: your memory, calendar, email, and tools.
5
+ * One-shot: atris ask what should I do today
6
+ * Interactive: atris ask
7
+ */
8
+
9
+ const readline = require('readline');
10
+ const { getApiBaseUrl, httpRequest, apiRequestJson } = require('../utils/api');
11
+ const auth = require('../utils/auth');
12
+
13
+ const TIMEOUT_MS = 300000;
14
+
15
+ function stripEvents(text) {
16
+ // Drop inline [[EVENT]]{...} tool chatter; keep only the spoken reply.
17
+ // Tool names are surfaced dimmed in the terminal (texts never see them).
18
+ if (typeof text !== 'string') return text;
19
+ const parts = [];
20
+ const toolNames = [];
21
+ for (let chunk of text.split('[[EVENT]]')) {
22
+ const trimmed = chunk.trimStart();
23
+ if (trimmed.startsWith('{')) {
24
+ let depth = 0;
25
+ let inString = false;
26
+ let escaped = false;
27
+ let end = -1;
28
+ for (let i = 0; i < trimmed.length; i += 1) {
29
+ const ch = trimmed[i];
30
+ if (escaped) { escaped = false; continue; }
31
+ if (ch === '\\') { escaped = true; continue; }
32
+ if (ch === '"') inString = !inString;
33
+ if (inString) continue;
34
+ if (ch === '{') depth += 1;
35
+ if (ch === '}') { depth -= 1; if (depth === 0) { end = i + 1; break; } }
36
+ }
37
+ if (end > 0) {
38
+ try {
39
+ const evt = JSON.parse(trimmed.slice(0, end));
40
+ if (evt.type === 'tool_call' && evt.tool) toolNames.push(evt.tool);
41
+ } catch (_) { /* not JSON, keep going */ }
42
+ chunk = trimmed.slice(end);
43
+ }
44
+ }
45
+ parts.push(chunk);
46
+ }
47
+ const reply = parts.join('').trim();
48
+ if (toolNames.length && process.stdout.isTTY) {
49
+ process.stdout.write(`\x1b[2m[⚙ ${[...new Set(toolNames)].join(', ')}]\x1b[0m\n`);
50
+ }
51
+ return reply;
52
+ }
53
+
54
+ function startSpinner(label) {
55
+ if (!process.stdout.isTTY) return { stop: () => {} };
56
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
57
+ const started = Date.now();
58
+ let i = 0;
59
+ const timer = setInterval(() => {
60
+ const secs = Math.floor((Date.now() - started) / 1000);
61
+ process.stdout.write(`\r${frames[i % frames.length]} ${label}${secs > 2 ? ` · ${secs}s` : ''} `);
62
+ i += 1;
63
+ }, 90);
64
+ return {
65
+ stop: () => {
66
+ clearInterval(timer);
67
+ process.stdout.write('\r\x1b[2K');
68
+ },
69
+ };
70
+ }
71
+
72
+ async function typeOut(text) {
73
+ if (!process.stdout.isTTY || text.length > 4000) {
74
+ process.stdout.write(`${text}\n`);
75
+ return;
76
+ }
77
+ for (const ch of text) {
78
+ process.stdout.write(ch);
79
+ // eslint-disable-next-line no-await-in-loop
80
+ await new Promise((r) => setTimeout(r, ch === '\n' ? 12 : 3));
81
+ }
82
+ process.stdout.write('\n');
83
+ }
84
+
85
+ async function sendMessage(token, message) {
86
+ const res = await httpRequest(`${getApiBaseUrl()}/message/incoming`, {
87
+ method: 'POST',
88
+ headers: {
89
+ Authorization: `Bearer ${token}`,
90
+ 'Content-Type': 'application/json',
91
+ },
92
+ body: JSON.stringify({ message, source: 'cli' }),
93
+ timeoutMs: TIMEOUT_MS,
94
+ });
95
+ if (res.status !== 200) {
96
+ let detail = res.body.toString('utf8').slice(0, 200);
97
+ try { detail = JSON.parse(detail).detail || detail; } catch (_) { /* raw */ }
98
+ throw new Error(`HTTP ${res.status}: ${detail}`);
99
+ }
100
+ return JSON.parse(res.body.toString('utf8'));
101
+ }
102
+
103
+ async function askAtris(args) {
104
+ const ensured = await auth.ensureValidCredentials(apiRequestJson);
105
+ if (ensured.error === 'not_logged_in' || !ensured.credentials?.token) {
106
+ console.error('✗ Not logged in. Run "atris login" first.');
107
+ process.exit(1);
108
+ }
109
+ if (ensured.error) {
110
+ console.error(`✗ Authentication failed: ${ensured.detail || ensured.error}. Run "atris login".`);
111
+ process.exit(1);
112
+ }
113
+ const { token } = ensured.credentials;
114
+
115
+ const oneShot = (args || []).join(' ').trim();
116
+ if (oneShot === '-h' || oneShot === '--help' || oneShot === 'help') {
117
+ console.log('Usage: atris ask ["message"]');
118
+ console.log('');
119
+ console.log(' Chat with your personal Atris computer: memory, calendar, email, tools.');
120
+ console.log('');
121
+ console.log(' atris ask Interactive chat');
122
+ console.log(' atris ask what is on today One-shot question');
123
+ return;
124
+ }
125
+
126
+ if (oneShot) {
127
+ const spinner = startSpinner('thinking');
128
+ try {
129
+ const body = await sendMessage(token, oneShot);
130
+ spinner.stop();
131
+ await typeOut(stripEvents(body.response) || JSON.stringify(body));
132
+ } catch (err) {
133
+ spinner.stop();
134
+ console.error(`✗ ${err.message}`);
135
+ process.exit(1);
136
+ }
137
+ return;
138
+ }
139
+
140
+ console.log('Chatting with your computer. Ctrl-C or "bye" to leave.\n');
141
+ await new Promise((resolve) => {
142
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
+ let busy = false;
144
+ let closed = false;
145
+ const prompt = () => { if (!closed) rl.question('you › ', onLine); };
146
+ async function onLine(line) {
147
+ const message = line.trim();
148
+ if (!message) { prompt(); return; }
149
+ if (['bye', 'exit', 'quit'].includes(message.toLowerCase())) { rl.close(); return; }
150
+ busy = true;
151
+ const spinner = startSpinner('thinking');
152
+ try {
153
+ const body = await sendMessage(token, message);
154
+ spinner.stop();
155
+ process.stdout.write('\natris › ');
156
+ await typeOut(stripEvents(body.response) || JSON.stringify(body));
157
+ process.stdout.write('\n');
158
+ } catch (err) {
159
+ spinner.stop();
160
+ console.error(`✗ ${err.message}\n`);
161
+ }
162
+ busy = false;
163
+ if (closed) { console.log('\nbye.'); resolve(); return; }
164
+ prompt();
165
+ }
166
+ rl.on('close', () => {
167
+ closed = true;
168
+ // Let an in-flight answer finish printing before we leave.
169
+ if (!busy) { console.log('\nbye.'); resolve(); }
170
+ });
171
+ prompt();
172
+ });
173
+ }
174
+
175
+ module.exports = { askAtris };
package/commands/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- const { AGENT_TOKEN_EXPIRED_DETAIL, loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, ensureValidCredentials, loadProfile, listProfiles, profileNameFromEmail, deleteProfile, saveProfile, getTokenExpiryEpochSeconds, getTerminalSessionId, setSessionProfile, getSessionProfile, clearSessionProfile, cleanStaleSessions, getSessionsDir } = require('../utils/auth');
1
+ const { AGENT_TOKEN_EXPIRED_DETAIL, decodeJwtClaims, 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
3
  const { isNonInteractive, wantsJson } = require('../lib/noninteractive');
4
4
  const { hasFlag, readFlag } = require('../lib/arg-parser');
@@ -85,20 +85,45 @@ function extractAgentTokenMeta(data, requested, token) {
85
85
  return { scopes, dailyCreditCap, expiresAt };
86
86
  }
87
87
 
88
+ function isAgentAccessToken(token) {
89
+ return decodeJwtClaims(token)?.type === 'agent_access';
90
+ }
91
+
92
+ function scopedTokenCandidate(credentials = {}) {
93
+ if (credentials.agent_token) return credentials.agent_token;
94
+ if (
95
+ credentials.source === 'env'
96
+ || credentials.source === 'agent_token_file'
97
+ || isAgentAccessToken(credentials.token)
98
+ ) {
99
+ return firstNonEmptyString(credentials.token);
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function canMintFromLogin(credentials = {}) {
105
+ const login = firstNonEmptyString(credentials.token);
106
+ const refresh = firstNonEmptyString(credentials.refresh_token);
107
+ if (isAgentAccessToken(login) && !refresh) return false;
108
+ return Boolean(login || refresh);
109
+ }
110
+
88
111
  function persistMintedAgentToken(credentials, token, extras = {}) {
112
+ if (isAgentAccessToken(credentials.token)) {
113
+ throw new Error('Refusing to save a scoped agent token as the login token; keep it under agent_token');
114
+ }
89
115
  const next = {
90
- token,
116
+ ...credentials,
91
117
  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(),
118
+ agent_token: token,
119
+ agent_token_scopes: extras.scopes || [],
120
+ agent_token_expires_at: extras.expiresAt || null,
96
121
  };
97
122
  if (credentials.source_profile) {
98
123
  saveProfile(credentials.source_profile, next);
99
124
  return next;
100
125
  }
101
- saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider);
126
+ saveCredentials(next.token, next.refresh_token, next.email, next.user_id, next.provider, next);
102
127
  return next;
103
128
  }
104
129
 
@@ -139,10 +164,10 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
139
164
  ? requested.dailyCreditCap
140
165
  : DEFAULT_DAILY_CREDIT_CAP;
141
166
 
142
- const credentials = load() || {};
167
+ const credentials = await load(api) || {};
143
168
  const accessToken = firstNonEmptyString(credentials.token);
144
169
  const refreshToken = firstNonEmptyString(credentials.refresh_token);
145
- if (!accessToken && !refreshToken) {
170
+ if (!canMintFromLogin(credentials)) {
146
171
  return { ok: false, code: 'not_logged_in', error: NO_STORED_JWT_MESSAGE };
147
172
  }
148
173
 
@@ -150,7 +175,7 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
150
175
  scopes,
151
176
  daily_credit_cap: dailyCreditCap,
152
177
  };
153
- let authToken = accessToken || refreshToken;
178
+ let authToken = isAgentAccessToken(accessToken) ? refreshToken : (accessToken || refreshToken);
154
179
  let result = await postAgentToken(api, authToken, body);
155
180
  if (!result.ok && result.status === 401 && refreshToken && authToken !== refreshToken) {
156
181
  authToken = refreshToken;
@@ -172,7 +197,9 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
172
197
  return { ok: false, code: 'missing_token', error: 'backend did not return an agent token' };
173
198
  }
174
199
 
200
+ const meta = extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted);
175
201
  persist(credentials, minted, {
202
+ ...meta,
176
203
  refresh_token: firstNonEmptyString(result.data && result.data.refresh_token) || refreshToken,
177
204
  saved_at: now(),
178
205
  });
@@ -180,7 +207,8 @@ async function mintScopedAgentToken(requested = {}, deps = {}) {
180
207
  return {
181
208
  ok: true,
182
209
  token: minted,
183
- meta: extractAgentTokenMeta(result.data, { scopes, dailyCreditCap }, minted),
210
+ meta,
211
+ storedIn: credentials.source_profile ? `profile ${credentials.source_profile}, agent_token` : '~/.atris/credentials.json, agent_token',
184
212
  };
185
213
  }
186
214
 
@@ -191,24 +219,21 @@ async function ensureBilledCommandAuth(scope, deps = {}) {
191
219
  }
192
220
 
193
221
  const api = deps.apiRequestJson || apiRequestJson;
194
- const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
195
222
  const load = deps.loadCredentials || loadCredentials;
196
223
  const mint = deps.mintScopedAgentToken || mintScopedAgentToken;
197
224
 
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
- }
225
+ const ensured = !deps.forceMint && deps.ensureValidCredentials
226
+ ? await deps.ensureValidCredentials(api) : null;
227
+ const credentials = ensured?.credentials || await load(api) || {};
228
+ const candidate = scopedTokenCandidate(credentials);
229
+ const claims = decodeJwtClaims(candidate);
230
+ const scopes = claims?.scopes || credentials.agent_token_scopes || [];
231
+ const expiry = claims?.exp ? claims.exp * 1000 : Date.parse(credentials.agent_token_expires_at);
232
+ if (!deps.forceMint && candidate && Array.isArray(scopes) && scopes.includes(wanted) && Number.isFinite(expiry) && expiry > Date.now()) {
233
+ return { ok: true, token: candidate, minted: false, credentials };
208
234
  }
209
235
 
210
- const credentials = load() || {};
211
- if (!firstNonEmptyString(credentials.token, credentials.refresh_token)) {
236
+ if (!canMintFromLogin(credentials)) {
212
237
  return { ok: false, error: NO_STORED_JWT_MESSAGE };
213
238
  }
214
239
 
@@ -267,6 +292,7 @@ async function mintAgentToken(args = [], deps = {}) {
267
292
  output(JSON.stringify({
268
293
  ok: true,
269
294
  minted: true,
295
+ stored_in: minted.storedIn,
270
296
  scopes: minted.meta.scopes,
271
297
  daily_credit_cap: minted.meta.dailyCreditCap,
272
298
  expires_at: minted.meta.expiresAt,
@@ -275,6 +301,7 @@ async function mintAgentToken(args = [], deps = {}) {
275
301
  }
276
302
 
277
303
  printAgentTokenMint(minted.meta, output);
304
+ output(`stored in ${minted.storedIn}`);
278
305
  return 0;
279
306
  }
280
307
 
@@ -565,7 +592,7 @@ async function switchAccount() {
565
592
  const profile = loadProfile(name);
566
593
  const email = profile?.email || 'unknown';
567
594
  const marker = name === currentName ? ' ← active' : '';
568
- console.log(` ${i + 1}. ${name} ${email}${marker}`);
595
+ console.log(` ${i + 1}. ${name}, ${email}${marker}`);
569
596
  });
570
597
  console.log(` ${profiles.length + 1}. Add new account`);
571
598
  console.log(` ${profiles.length + 2}. Cancel`);
@@ -620,13 +647,13 @@ function activateProfile(name, currentName, { global = false } = {}) {
620
647
  }
621
648
 
622
649
  if (global || !getTerminalSessionId()) {
623
- // Global switch write to credentials.json (affects all terminals)
650
+ // Global switch, write to credentials.json (affects all terminals)
624
651
  const credentialsPath = getCredentialsPath();
625
652
  fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
626
653
  try { fs.chmodSync(credentialsPath, 0o600); } catch {}
627
- console.log(`Switched to ${profile.email || name} (global all terminals)`);
654
+ console.log(`Switched to ${profile.email || name} (global, all terminals)`);
628
655
  } else {
629
- // Per-terminal switch write session file (only this terminal)
656
+ // Per-terminal switch, write session file (only this terminal)
630
657
  setSessionProfile(name);
631
658
  console.log(`Switched to ${profile.email || name}`);
632
659
  }
@@ -682,7 +709,7 @@ function useAccount() {
682
709
  if (!process.stdout.isTTY) {
683
710
  process.stdout.write(`export ATRIS_PROFILE=${match}\n`);
684
711
  } else {
685
- // Interactive print instructions
712
+ // Interactive, print instructions
686
713
  console.log(`export ATRIS_PROFILE=${match}`);
687
714
  console.log(`\n# Run this to activate ${email} in this terminal:`);
688
715
  console.log(`# eval "$(atris use ${targetName})"`);
@@ -893,7 +920,7 @@ function switchSession() {
893
920
  if (termId) {
894
921
  setSessionProfile(name);
895
922
  } else {
896
- // No terminal ID global fallback
923
+ // No terminal ID, global fallback
897
924
  const credentialsPath = getCredentialsPath();
898
925
  fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
899
926
  try { fs.chmodSync(credentialsPath, 0o600); } catch {}
@@ -910,7 +937,7 @@ function shellInit() {
910
937
  '# Atris per-terminal account switching',
911
938
  '# Added by: eval "$(atris shell-init)"',
912
939
  '_atris_session_id() {',
913
- ' # Detect terminal session ID same priority as the Node.js code',
940
+ ' # Detect terminal session ID, same priority as the Node.js code',
914
941
  ' local _sid="${TERM_SESSION_ID:-${ITERM_SESSION_ID:-${TMUX_PANE:-${WT_SESSION:-${WEZTERM_PANE:-}}}}}"',
915
942
  ' if [[ -n "$_sid" ]]; then echo "$_sid"; return; fi',
916
943
  ' # Fallback: TTY device (unique per macOS/Linux terminal tab)',
@@ -982,6 +1009,7 @@ module.exports = {
982
1009
  shellInit,
983
1010
  parseAgentTokenArgs,
984
1011
  mintAgentToken,
1012
+ persistMintedAgentToken,
985
1013
  ensureBilledCommandAuth,
986
1014
  wantsAgentToken,
987
1015
  };
@@ -752,7 +752,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
752
752
  // the landing gate reuse the live certification verifier result without
753
753
  // persisting trust across heartbeats. Denied lanes and check-less proofs wait.
754
754
  // No hardcoded --limit here: a fixed low cap (this used to be 12) silently
755
- // undercounts a real backlog every single tick 12/hour forever even with
755
+ // undercounts a real backlog every single tick: 12/hour forever even with
756
756
  // 78 certified rows waiting. Let `atris task auto-accept-certified` apply
757
757
  // its own default (12 without --all, a high safety cap under --all) so a
758
758
  // policy with accept_all:true actually drains the full certified set.
@@ -763,7 +763,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
763
763
  try {
764
764
  const parsed = JSON.parse(accept.stdout);
765
765
  const results = Array.isArray(parsed.results) ? parsed.results : [];
766
- // A refused sweep (ok:false a guard tripped, policy race) carries no
766
+ // A refused sweep (ok:false, a guard tripped, policy race) carries no
767
767
  // summary fields. Name the reason instead of leaving nulls that read as
768
768
  // "no work": a blind heartbeat must say WHY it is blind.
769
769
  if (parsed.ok === false) {
@@ -841,7 +841,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
841
841
  }
842
842
 
843
843
  // 3b. janitor, every tick: a mission left paused past 48h and a worktree
844
- // already merged into base are the same rot pattern as an unlanded branch
844
+ // already merged into base are the same rot pattern as an unlanded branch,
845
845
  // left alone they accumulate until a human runs `mission stop` and
846
846
  // `worktree cleanup --apply` by hand, which is exactly the chore autoland
847
847
  // exists to remove. Off switch: policy.janitor === false (default on, the
@@ -880,6 +880,30 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
880
880
  recordJanitorState(state, { stopped: stoppedMissions, held: heldMissions, worktrees: receipt.worktrees_reaped });
881
881
  }
882
882
 
883
+ // 3b2. green landing, every tick: one stale branch that merges clean and
884
+ // passes the full verify in a scratch checkout lands on base and is pushed.
885
+ // A red branch opens a close flag the same hour instead of waiting for the
886
+ // TTL reap to salvage it. Off by policy: green_landing:false.
887
+ if (policy.green_landing !== false) {
888
+ try {
889
+ const { landStaleGreenBranches, summaryLine } = require('../lib/land-green');
890
+ const green = landStaleGreenBranches(root, {
891
+ verifyCommand: policy.verify_command || undefined,
892
+ limit: Number(policy.green_landing_limit) > 0 ? Number(policy.green_landing_limit) : 1,
893
+ });
894
+ receipt.green_landing = {
895
+ candidates: green.candidates.length,
896
+ landed: green.landed,
897
+ red: green.red,
898
+ verdicts: green.verdicts.map((v) => ({ branch: v.branch, action: v.action, reason: v.reason || null, detail: v.detail || null, failing: v.verify ? v.verify.failing : undefined })),
899
+ summary: summaryLine(green),
900
+ };
901
+ state.green_landing = { at: green.at, ...receipt.green_landing };
902
+ } catch (err) {
903
+ receipt.green_landing_error = String((err && err.message) || err).slice(0, 200);
904
+ }
905
+ }
906
+
883
907
  // 3c. landing sweep, every tick: merged branch residue is cleared,
884
908
  // TTL-expired work is salvaged before deletion, and 48h-stale active work
885
909
  // is carried into the digest instead of silently aging forever.
@@ -952,7 +976,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
952
976
  }
953
977
  }
954
978
 
955
- // 5b. daily keep/revert experiment self-gated inside experiments daily;
979
+ // 5b. daily keep/revert experiment, self-gated inside experiments daily;
956
980
  // hourly ticks are harmless no-ops after the first run each day.
957
981
  if (policy.daily_experiment !== false) {
958
982
  const daily = runOwnCli(root, ['experiments', 'daily', '--json']);
@@ -1021,7 +1045,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
1021
1045
  // Deltas alone ("0 certified, 0 landed") are true and useless: they read
1022
1046
  // as "nothing to do" whether the queue is empty or 19-deep and wedged.
1023
1047
  // Always print the standing backlog, and name work that cleared every
1024
- // gate and still did not land that is the shape of a stuck loop.
1048
+ // gate and still did not land. That is the shape of a stuck loop.
1025
1049
  const stuckNote = stuckBacklogNote(receipt);
1026
1050
  const summary = `autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms${stuckNote}, digest ${digestTickStatus(receipt)}${reapNote}${janitorNote}${heldNote}${wishNote}${engineSweepNote}${receiptNote}`;
1027
1051
  const validationLine = engineAnswerValidationLine(receipt);
@@ -1,6 +1,6 @@
1
1
  // atris autopilot: point it at the workspace and it keeps going.
2
- // Each leg picks the most logical work a moving mission first, then a
3
- // member choosing useful work, then a self-chosen mission and drives it
2
+ // Each leg picks the most logical work, a moving mission first, then a
3
+ // member choosing useful work, then a self-chosen mission, and drives it
4
4
  // through the mission runtime. It loops until stopped:
5
5
  // atris autopilot stop (from any terminal) or Ctrl-C here.
6
6
  // The old suggest→justify→execute loop lives on behind `atris autopilot --legacy`.
@@ -57,7 +57,7 @@ function positiveNumber(value) {
57
57
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
58
58
  }
59
59
 
60
- // `--iterations=N` or `--iterations N` the leg cap legacy callers pass.
60
+ // `--iterations=N` or `--iterations N`, the leg cap legacy callers pass.
61
61
  function maxLegsFlag(args) {
62
62
  const eq = args.find((a) => typeof a === 'string' && a.startsWith('--iterations='));
63
63
  if (eq) return positiveNumber(eq.split('=')[1]);
@@ -187,7 +187,7 @@ function autopilotStatus(root = process.cwd()) {
187
187
  const state = readState(root);
188
188
  if (!state) { console.log('Autopilot is not running.'); return 0; }
189
189
  const alive = pidAlive(state.pid);
190
- console.log(`Autopilot ${alive ? 'running' : 'not running (stale state)'} pid ${state.pid}, started ${state.started_at}, legs ${state.legs || 0}`);
190
+ console.log(`Autopilot ${alive ? 'running' : 'not running (stale state)'}, pid ${state.pid}, started ${state.started_at}, legs ${state.legs || 0}`);
191
191
  if (state.current_leg) console.log(`Current leg: ${state.current_leg}`);
192
192
  if (!alive) clearState(root);
193
193
  return 0;
@@ -276,7 +276,7 @@ async function autopilotFront(args = []) {
276
276
 
277
277
  const current = { child: null };
278
278
  // `autopilot stop` SIGTERMs this pid directly, so the farewell must print
279
- // here the signal always beats the loop's own stop-file check.
279
+ // here, the signal always beats the loop's own stop-file check.
280
280
  const onSignal = () => {
281
281
  if (current.child) { try { current.child.kill('SIGTERM'); } catch {} }
282
282
  const reason = stopRequested(root) ? 'stop requested' : 'interrupted';
@@ -314,7 +314,7 @@ async function autopilotFront(args = []) {
314
314
  if (maxLegs && state.legs >= maxLegs) { stopReason = `leg cap reached (--iterations=${maxLegs})`; break; }
315
315
  if (stopRequested(root)) { stopReason = 'stop requested'; break; }
316
316
 
317
- // A leg that finishes in seconds means no real work happened a crash,
317
+ // A leg that finishes in seconds means no real work happened, a crash,
318
318
  // or a degenerate mission whose ticks record instantly (exit 0). Either
319
319
  // way, back off and stop instead of hot-looping on it.
320
320
  const legSeconds = (Date.now() - legStarted) / 1000;