atris 3.30.7 → 3.30.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,7 +91,7 @@ Integrates with any agent.
91
91
 
92
92
  ```bash
93
93
  cd your-project
94
- ax --fast --chat
94
+ ax chat ax --fast
95
95
  ax --fast "hello"
96
96
  ax --pro "help me plan this task"
97
97
  ax --max "reason through this hard decision"
package/ax CHANGED
@@ -9,6 +9,7 @@ const readline = require('readline');
9
9
  const crypto = require('crypto');
10
10
  const { Readable } = require('stream');
11
11
  const { loadCredentials } = require('./utils/auth');
12
+ const missionRuntime = require('./lib/mission-runtime-loop');
12
13
 
13
14
  const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
14
15
  const BACKEND = {
@@ -16,8 +17,8 @@ const BACKEND = {
16
17
  port: 8000,
17
18
  path: '/api/atris2/turn'
18
19
  };
19
- const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
20
20
  const CLOUD_BACKEND_BASE = 'https://api.atris.ai';
21
+ const LOCAL_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
21
22
  const CODE_FAST = {
22
23
  path: '/api/cursor/turn',
23
24
  model: 'composer-2-5-fast',
@@ -125,11 +126,11 @@ function formatUsage() {
125
126
  ' ax [--max|--fast] --benchmark',
126
127
  '',
127
128
  'Modes:',
128
- ' --max highest reasoning, slowest turns',
129
- ' --pro deeper tool loop',
130
- ' --fast faster low-latency turns',
129
+ ' --max hosted Atris 2, highest reasoning, slowest turns',
130
+ ' --pro hosted Atris 2, deeper tool loop',
131
+ ' --fast hosted Atris 2, faster low-latency turns',
131
132
  ' --code-fast Atris Code Fast public lane',
132
- ' --local force local workspace tools with your own AtrisOS backend',
133
+ ' --local opt into local backend/workspace tools',
133
134
  ' --cloud force authenticated cloud connectors/chat',
134
135
  ' --business <slug> run tools on that business cloud workspace (EC2)',
135
136
  ' --verify <cmd> gate the turn on this command passing (default: no verifier)',
@@ -211,11 +212,12 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
211
212
  }
212
213
 
213
214
  function backendBaseUrl(options = {}) {
214
- const explicitBase = process.env.AX_BACKEND_URL
215
- || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
216
- || process.env.OBELISK_ATRIS2_BACKEND_URL;
217
- if (explicitBase) return explicitBase.replace(/\/$/, '');
218
- return options.route === 'cloud' ? CLOUD_BACKEND_BASE : DEFAULT_BACKEND_BASE;
215
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
216
+ if (process.env.AX_BACKEND_URL) return process.env.AX_BACKEND_URL.replace(/\/$/, '');
217
+ if (route === 'local') {
218
+ return (process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL || LOCAL_BACKEND_BASE).replace(/\/$/, '');
219
+ }
220
+ return (process.env.OBELISK_ATRIS2_BACKEND_URL || process.env.ATRIS_API_BASE || CLOUD_BACKEND_BASE).replace(/\/$/, '');
219
221
  }
220
222
 
221
223
  function backendUrl(options = {}) {
@@ -268,9 +270,13 @@ function buildRunProfile(options = {}) {
268
270
  reasoning: 'Composer 2.5 fast lane; charges 10 credits per public turn'
269
271
  };
270
272
  }
271
- const profileMessage = options.message || 'what files are here?';
272
- const route = resolveRoute(profileMessage, options);
273
- const payload = buildPayload(profileMessage, { mode, cwd, route });
273
+ const route = resolveRoute(options.message || 'doctor', options);
274
+ const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
275
+ const reasoningByMode = mode === 'max'
276
+ ? 'Max workspace tool loop uses high reasoning effort'
277
+ : mode === 'pro'
278
+ ? 'Pro workspace tool loop uses API default medium'
279
+ : 'Fast workspace tool loop uses provider default';
274
280
  return {
275
281
  endpoint: backendUrl({ route }),
276
282
  mode,
@@ -278,13 +284,11 @@ function buildRunProfile(options = {}) {
278
284
  model: payload.model,
279
285
  workspace_path: payload.workspace_path || 'cloud',
280
286
  max_turns: payload.max_turns,
287
+ member_slug: 'ax',
288
+ bypass_permissions: false,
281
289
  streaming: true,
282
290
  runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
283
- reasoning: mode === 'max'
284
- ? 'backend reports run row; Max workspace tool loop uses high reasoning effort'
285
- : mode === 'pro'
286
- ? 'backend reports run row; Pro workspace tool loop uses API default medium'
287
- : 'backend reports run row; Fast workspace tool loop uses provider default'
291
+ reasoning: `${route === 'cloud' ? 'Atris cloud service' : 'local backend'}; ${reasoningByMode}`
288
292
  };
289
293
  }
290
294
 
@@ -303,18 +307,23 @@ function formatRunProfile(profile, options = {}) {
303
307
  }
304
308
 
305
309
  async function buildRuntimeHealth(options = {}) {
310
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
306
311
  const healthRes = options.healthRes
307
312
  ? await Promise.resolve(options.healthRes)
308
- : await requestJson(ATRIS2_HEALTH_PATH, { token: '', timeoutMs: options.timeoutMs || 1500, route: options.route || 'local' });
313
+ : await requestJson(ATRIS2_HEALTH_PATH, { token: route === 'cloud' ? authToken() : '', timeoutMs: options.timeoutMs || 1500, route });
309
314
  const data = healthRes.ok && healthRes.data && typeof healthRes.data === 'object' ? healthRes.data : {};
310
315
  const models = Array.isArray(data.models) ? data.models : [];
311
316
  const fast = models.find(row => row && row.id === 'atris:fast') || null;
317
+ const status = Number(healthRes.status || 0);
318
+ const authRequired = status === 401 || status === 403;
312
319
  return {
313
320
  schema: 'ax.runtime_health.v1',
321
+ route,
314
322
  backend: {
315
323
  ready: Boolean(healthRes.ok && data.ready),
316
- reachable: Boolean(healthRes.ok),
317
- status: healthRes.status || 0,
324
+ reachable: Boolean(healthRes.ok || status > 0),
325
+ auth_required: authRequired,
326
+ status,
318
327
  error: healthRes.error || '',
319
328
  },
320
329
  fast: {
@@ -331,14 +340,19 @@ async function buildRuntimeHealth(options = {}) {
331
340
  function formatRuntimeHealth(health, options = {}) {
332
341
  const backendReady = health && health.backend && health.backend.ready;
333
342
  const backendReachable = health && health.backend && health.backend.reachable;
343
+ const authRequired = health && health.backend && health.backend.auth_required;
334
344
  const fastReady = health && health.fast && health.fast.ready;
335
345
  const permissionReady = health && health.permissions && health.permissions.ready;
336
346
  const rows = [
337
- ['backend', backendReady ? 'ready' : backendReachable ? 'not ready' : 'offline'],
347
+ ['backend', authRequired ? 'auth required' : backendReady ? 'ready' : backendReachable ? 'not ready' : 'offline'],
338
348
  ['fast', fastReady ? 'ready' : 'not ready'],
339
- ['approvals', permissionReady ? 'ready' : 'offline'],
349
+ ['approvals', authRequired ? 'auth required' : permissionReady ? 'ready' : 'offline'],
340
350
  ];
341
- if (!backendReachable) rows.push(['fix', 'local backend offline; use hosted mode or start your AtrisOS backend']);
351
+ if (authRequired) {
352
+ rows.push(['fix', 'run atris login, then rerun ax --doctor']);
353
+ } else if (!backendReachable) {
354
+ rows.push(['fix', health?.route === 'local' ? 'start local backend, then rerun ax --doctor' : 'Atris cloud unavailable; retry or run ax --doctor']);
355
+ }
342
356
  return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
343
357
  }
344
358
 
@@ -527,7 +541,7 @@ function cachedIntegrationStatus(options = {}) {
527
541
 
528
542
  async function buildConnectionContext(options = {}) {
529
543
  const token = options.token || authToken();
530
- const localStatusUserId = isLoopbackBackend({ route: options.route }) ? authUserId() : '';
544
+ const localStatusUserId = isLoopbackBackend() ? authUserId() : '';
531
545
  const statusPath = localStatusUserId
532
546
  ? `${ATRIS2_CONNECTION_STATUS_PATH}?connection_user_id=${encodeURIComponent(localStatusUserId)}`
533
547
  : CONNECTION_STATUS_PATH;
@@ -535,9 +549,9 @@ async function buildConnectionContext(options = {}) {
535
549
  options.statusRes
536
550
  ? Promise.resolve(options.statusRes)
537
551
  : localStatusUserId || token
538
- ? requestJson(statusPath, { token: localStatusUserId ? '' : token, route: options.route })
552
+ ? requestJson(statusPath, { token: localStatusUserId ? '' : token })
539
553
  : Promise.resolve({ ok: false, data: null }),
540
- options.contractRes ? Promise.resolve(options.contractRes) : requestJson(CONNECTION_CAPABILITIES_PATH, { token: '', route: options.route })
554
+ options.contractRes ? Promise.resolve(options.contractRes) : requestJson(CONNECTION_CAPABILITIES_PATH, { token: '' })
541
555
  ]);
542
556
  const statusData = statusRes.ok && statusRes.data && typeof statusRes.data === 'object' ? statusRes.data : {};
543
557
  const backendStatuses = statusData.statuses && typeof statusData.statuses === 'object' ? statusData.statuses : statusData;
@@ -610,17 +624,9 @@ function githubWorkspaceIntent(message) {
610
624
  function resolveRoute(message, options = {}) {
611
625
  if (options.route === 'local' || options.forceLocal) return 'local';
612
626
  if (options.route === 'cloud' || options.forceCloud) return 'cloud';
613
- if (githubWorkspaceIntent(message)) return 'local';
614
- if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
615
- if (workspaceIntent(message)) return 'local';
616
627
  return 'cloud';
617
628
  }
618
629
 
619
- function shouldPreflightRuntime(route, options = {}, mode = 'fast') {
620
- if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') return false;
621
- return route !== 'cloud';
622
- }
623
-
624
630
  function normalizeMode(mode) {
625
631
  if (mode === 'code-fast' || mode === 'code') return 'code-fast';
626
632
  if (mode === 'max') return 'max';
@@ -692,6 +698,161 @@ function formatDoneLine(ms, credits) {
692
698
  return `${base} —`;
693
699
  }
694
700
 
701
+ function stripMissionQuotes(value) {
702
+ const text = String(value || '').trim();
703
+ if (text.length >= 2) {
704
+ const first = text[0];
705
+ const last = text[text.length - 1];
706
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
707
+ return text.slice(1, -1).trim();
708
+ }
709
+ }
710
+ return text;
711
+ }
712
+
713
+ function extractMissionFlag(text, flagName) {
714
+ const source = String(text || '');
715
+ const inline = new RegExp(`(^|\\s)${flagName}=([^\\s]+)(?=\\s|$)`).exec(source);
716
+ if (inline) {
717
+ return {
718
+ value: inline[2],
719
+ text: `${source.slice(0, inline.index)}${inline[1] || ''}${source.slice(inline.index + inline[0].length)}`.replace(/\s+/g, ' ').trim(),
720
+ };
721
+ }
722
+ const split = new RegExp(`(^|\\s)${flagName}\\s+([^\\s]+)(?=\\s|$)`).exec(source);
723
+ if (split) {
724
+ return {
725
+ value: split[2],
726
+ text: `${source.slice(0, split.index)}${split[1] || ''}${source.slice(split.index + split[0].length)}`.replace(/\s+/g, ' ').trim(),
727
+ };
728
+ }
729
+ return { value: null, text: source.trim() };
730
+ }
731
+
732
+ function missionRunIntentFromMessage(message) {
733
+ return missionRuntime.missionRunIntentFromMessage(message);
734
+ }
735
+
736
+ function loadMissionRecord(cwd, missionId) {
737
+ if (!missionId) return null;
738
+ try {
739
+ const statePath = path.join(cwd || process.cwd(), '.atris', 'state', 'missions.jsonl');
740
+ if (!fs.existsSync(statePath)) return null;
741
+ const lines = fs.readFileSync(statePath, 'utf8').split('\n').filter(Boolean);
742
+ for (let i = lines.length - 1; i >= 0; i--) {
743
+ let record;
744
+ try {
745
+ record = JSON.parse(lines[i]);
746
+ } catch (_) {
747
+ continue;
748
+ }
749
+ const id = record && (record.id || record.mission_id);
750
+ if (id === missionId) return record;
751
+ }
752
+ } catch (_) {
753
+ return null;
754
+ }
755
+ return null;
756
+ }
757
+
758
+ function currentAtrisGoal(cwd = process.cwd()) {
759
+ try {
760
+ const statePath = path.join(cwd, '.atris', 'state', 'atris_goal.json');
761
+ if (!fs.existsSync(statePath)) return null;
762
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
763
+ const goal = state && state.goal;
764
+ if (!goal || !goal.objective) return null;
765
+ if (String(goal.runner || '').trim().toLowerCase() === 'codex_goal') return null;
766
+ const mission = loadMissionRecord(cwd, goal.mission_id);
767
+ return {
768
+ ...goal,
769
+ mission_status: mission?.status || goal.mission_status,
770
+ created_at: goal.created_at || mission?.created_at || null,
771
+ updated_at: goal.updated_at || mission?.updated_at || null,
772
+ completed_at: goal.completed_at || mission?.completed_at || null,
773
+ next_command: goal.next_command || mission?.next_action || null,
774
+ };
775
+ } catch (_) {
776
+ return null;
777
+ }
778
+ }
779
+
780
+ function goalElapsedSeconds(goal) {
781
+ const started = Date.parse(goal?.created_at || '');
782
+ if (!Number.isFinite(started)) return null;
783
+ const ended = Date.parse(goal?.completed_at || '') || Date.now();
784
+ return Math.max(0, Math.floor((ended - started) / 1000));
785
+ }
786
+
787
+ function goalAchieved(goal) {
788
+ return ['complete', 'completed', 'achieved'].includes(String(goal?.mission_status || goal?.status || '').toLowerCase());
789
+ }
790
+
791
+ function formatAtrisGoal(goal) {
792
+ if (!goal) return '';
793
+ const objective = String(goal.objective || '').length > 92
794
+ ? `${String(goal.objective).slice(0, 89)}...`
795
+ : String(goal.objective || '');
796
+ const elapsed = goalElapsedSeconds(goal);
797
+ const missionParts = [
798
+ goal.mission_id || '?',
799
+ goal.mission_status || '?',
800
+ goal.runner || 'mission',
801
+ ];
802
+ if (elapsed !== null) missionParts.push(`elapsed ${formatSeconds(elapsed)}`);
803
+ missionParts.push(`achieved ${goalAchieved(goal) ? 'yes' : 'no'}`);
804
+ return [
805
+ `Atris goal: ${objective}`,
806
+ `Mission: ${missionParts.join(' · ')}`,
807
+ goal.next_command ? `Next: ${goal.next_command}` : '',
808
+ ].filter(Boolean).join('\n');
809
+ }
810
+
811
+ function writeAtrisGoalBanner(output, cwd = process.cwd()) {
812
+ const text = formatAtrisGoal(currentAtrisGoal(cwd));
813
+ if (!text) return false;
814
+ output.write(`${text}\n\n`);
815
+ return true;
816
+ }
817
+
818
+ function maybeWriteAtrisGoalBanner(output, cwd = process.cwd()) {
819
+ if (process.env.AX_SHOW_GOAL_BANNER !== '1') return false;
820
+ return writeAtrisGoalBanner(output, cwd);
821
+ }
822
+
823
+ function formatMissionStartReceipt(payload) {
824
+ const mission = payload?.mission || {};
825
+ const goal = payload?.atris_goal_state?.goal || {};
826
+ const receiptGoal = {
827
+ ...goal,
828
+ objective: goal.objective || mission.objective,
829
+ mission_id: goal.mission_id || mission.id,
830
+ mission_status: goal.mission_status || mission.status,
831
+ runner: goal.runner || mission.runner,
832
+ created_at: goal.created_at || mission.created_at,
833
+ updated_at: goal.updated_at || mission.updated_at,
834
+ completed_at: goal.completed_at || mission.completed_at,
835
+ next_command: goal.next_command || payload?.next_command || mission.next_action,
836
+ };
837
+ const elapsed = goalElapsedSeconds(receiptGoal);
838
+ return [
839
+ 'Atris mission started',
840
+ `Goal: ${receiptGoal.objective || '?'}`,
841
+ `Mission: ${receiptGoal.mission_id || '?'} · ${receiptGoal.mission_status || '?'} · ${receiptGoal.runner || 'atris2'}`,
842
+ `Elapsed: ${elapsed === null ? '0s' : formatSeconds(elapsed)}`,
843
+ `Achieved: ${goalAchieved(receiptGoal) ? 'yes' : 'no'}`,
844
+ receiptGoal.next_command ? `Next: ${receiptGoal.next_command}` : '',
845
+ ].filter(Boolean).join('\n');
846
+ }
847
+
848
+ async function runLocalMission(intent, options = {}) {
849
+ return missionRuntime.runRuntimeMissionLoop(intent, {
850
+ cwd: options.cwd || process.cwd(),
851
+ cliPath: path.join(__dirname, 'bin', 'atris.js'),
852
+ onProgress: options.onProgress,
853
+ });
854
+ }
855
+
695
856
  function useColor(options = {}) {
696
857
  if (options.color === false) return false;
697
858
  if (process.env.NO_COLOR) return false;
@@ -1332,11 +1493,11 @@ function creditsFromApprovalExecution(response) {
1332
1493
  return Number.isFinite(credits) && credits > 0 ? credits : null;
1333
1494
  }
1334
1495
 
1335
- async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken(), route = 'cloud' } = {}) {
1496
+ async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken() } = {}) {
1336
1497
  return postJson(APPROVAL_EXECUTE_PATH, {
1337
1498
  model,
1338
1499
  approval_request: approvalRequest,
1339
- }, { token, route });
1500
+ }, { token });
1340
1501
  }
1341
1502
 
1342
1503
  async function executeApprovalRequest(approvalRequest, {
@@ -1719,15 +1880,16 @@ function handleEvent(event, state, output) {
1719
1880
  async function postTurn(message, options = {}) {
1720
1881
  const route = options.business ? 'local' : resolveRoute(message, options);
1721
1882
  const local = route !== 'cloud';
1883
+ const endpointRoute = options.business ? 'cloud' : route;
1722
1884
  const token = authToken();
1723
1885
  const shouldSendConnectionContext = options.connectionContext
1724
1886
  || route === 'cloud'
1725
1887
  || mentionsConnector(message)
1726
1888
  || /\b(can you use|can you access|connected|connections?|integrations?|tools?|capabilities)\b/i.test(message || '');
1727
1889
  const connectionContext = options.connectionContext || (shouldSendConnectionContext
1728
- ? await buildConnectionContext({ token, localWorkspace: local, route })
1890
+ ? await buildConnectionContext({ token, localWorkspace: local })
1729
1891
  : null);
1730
- const connectionUserId = !local && isLoopbackBackend({ route }) ? authUserId() : '';
1892
+ const connectionUserId = !local && isLoopbackBackend({ route: endpointRoute }) ? authUserId() : '';
1731
1893
  const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
1732
1894
  const postData = JSON.stringify(payload);
1733
1895
  const output = options.output || process.stdout;
@@ -1735,7 +1897,7 @@ async function postTurn(message, options = {}) {
1735
1897
  // SSE traffic in between, so the socket-idle timeout needs more headroom.
1736
1898
  const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1737
1899
  const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
1738
- const turnUrl = new URL(backendUrl({ route }));
1900
+ const turnUrl = new URL(backendUrl({ route: endpointRoute }));
1739
1901
  const transport = turnUrl.protocol === 'https:' ? https : http;
1740
1902
  const state = {
1741
1903
  events: [],
@@ -1756,7 +1918,7 @@ async function postTurn(message, options = {}) {
1756
1918
  relay: options.business ? {
1757
1919
  chain: Promise.resolve(),
1758
1920
  execute: options.business.executor,
1759
- post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl())
1921
+ post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl({ route: 'cloud' }))
1760
1922
  } : null
1761
1923
  };
1762
1924
 
@@ -1976,23 +2138,27 @@ async function chat(options = {}) {
1976
2138
 
1977
2139
  output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
1978
2140
  if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
1979
-
1980
- const runtimePreflight = { localReady: false };
1981
- const ensureRuntimeReady = async (routeForTurn = 'local') => {
1982
- if (!shouldPreflightRuntime(routeForTurn, options, mode)) {
2141
+ maybeWriteAtrisGoalBanner(output, cwd);
2142
+
2143
+ const runtimePreflight = new Set();
2144
+ const ensureRuntimeReady = async (routeForTurn = 'cloud') => {
2145
+ const endpointRoute = routeForTurn === 'local' ? 'local' : 'cloud';
2146
+ const key = `${normalizeMode(mode)}:${endpointRoute}`;
2147
+ if (runtimePreflight.has(key)) return true;
2148
+ if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') {
2149
+ runtimePreflight.add(key);
1983
2150
  return true;
1984
2151
  }
1985
- if (runtimePreflight.localReady) return true;
1986
2152
  const startedAt = Date.now();
1987
2153
  const health = options.runtimeHealth
1988
- ? await Promise.resolve(options.runtimeHealth({ mode }))
1989
- : await buildRuntimeHealth({ timeoutMs: options.preflightTimeoutMs || 1200, route: 'local' });
2154
+ ? await Promise.resolve(options.runtimeHealth({ mode, route: endpointRoute }))
2155
+ : await buildRuntimeHealth({ timeoutMs: options.preflightTimeoutMs || 1200, route: endpointRoute });
1990
2156
  if (runtimeReadyForChat(health, mode)) {
1991
- runtimePreflight.localReady = true;
2157
+ runtimePreflight.add(key);
1992
2158
  return true;
1993
2159
  }
1994
2160
  output.write(`${formatRuntimeHealth(health, output)}\n\n`);
1995
- output.write(`${formatBackendHint()}\n`);
2161
+ output.write(`${formatBackendHint({ route: endpointRoute })}\n`);
1996
2162
  output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
1997
2163
  return false;
1998
2164
  };
@@ -2022,6 +2188,26 @@ async function chat(options = {}) {
2022
2188
 
2023
2189
  if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
2024
2190
  output.write('\n');
2191
+
2192
+ const missionIntent = missionRunIntentFromMessage(trimmed);
2193
+ if (missionIntent) {
2194
+ const result = await runLocalMission(missionIntent, {
2195
+ cwd,
2196
+ onProgress: (line) => {
2197
+ if (/^pursuing /.test(line)) output.write('Pursuing goal...\n');
2198
+ else if (/^still_running /.test(line)) {
2199
+ const seconds = Number(String(line).split(/\s+/)[1]);
2200
+ output.write(`Still pursuing... ${formatSeconds(seconds)}\n`);
2201
+ } else if (/^landing /.test(line)) output.write('Landing goal...\n');
2202
+ else if (/^\[tick /.test(line)) output.write(`${line}\n`);
2203
+ },
2204
+ });
2205
+ output.write(`${result.text || 'Mission command failed.'}\n\n`);
2206
+ history.push({ role: 'user', content: trimmed });
2207
+ history.push({ role: 'assistant', content: result.text || 'Mission command failed.' });
2208
+ return false;
2209
+ }
2210
+
2025
2211
  const pendingApprovalRequest = latestPendingApprovalRequest(history);
2026
2212
  if (pendingApprovalRequest && normalizeMode(mode) !== 'code-fast' && messageCancelsPendingApproval(trimmed)) {
2027
2213
  const startedAt = Date.now();
@@ -2078,8 +2264,10 @@ async function chat(options = {}) {
2078
2264
  }
2079
2265
 
2080
2266
  const pendingTaskPreview = latestPendingTaskPreview(history);
2081
- const route = pendingTaskPreview ? 'cloud' : resolveRoute(trimmed, { route: options.route });
2082
- if (!(await ensureRuntimeReady(route))) return false;
2267
+ const route = pendingTaskPreview ? 'cloud' : options.route;
2268
+ maybeWriteAtrisGoalBanner(output, cwd);
2269
+ const turnRoute = options.business ? 'cloud' : resolveRoute(trimmed, { route });
2270
+ if (!(await ensureRuntimeReady(turnRoute))) return false;
2083
2271
  const turnFunction = options.turnFunction || turnFunctionForMode(mode);
2084
2272
  const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId });
2085
2273
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
@@ -2137,17 +2325,19 @@ async function chat(options = {}) {
2137
2325
  if (logger) logger.close(0);
2138
2326
  }
2139
2327
 
2140
- function printBackendHint() {
2328
+ function printBackendHint(options = {}) {
2141
2329
  console.log('');
2142
- console.log(formatBackendHint());
2330
+ console.log(formatBackendHint(options));
2143
2331
  }
2144
2332
 
2145
2333
  function backendStartCommand() {
2146
- return 'Run without --local for hosted chat, or set AX_BACKEND_URL to your AtrisOS backend.';
2334
+ return `cd /Users/keshavrao/arena/atrisos-backend/backend && ATRIS2_ALLOW_LOCAL_WORKSPACE=1 ENVIRONMENT=development ENV=development ../venv/bin/uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`;
2147
2335
  }
2148
2336
 
2149
- function formatBackendHint() {
2150
- return `Local workspace mode needs a running AtrisOS backend:\n${backendStartCommand()}`;
2337
+ function formatBackendHint(options = {}) {
2338
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
2339
+ if (route === 'local') return `Start backend:\n${backendStartCommand()}`;
2340
+ return 'Atris cloud did not respond. Check login/network, then rerun ax --doctor.';
2151
2341
  }
2152
2342
 
2153
2343
  function bufferedOutput() {
@@ -2250,7 +2440,7 @@ async function runSelfTest(options = {}) {
2250
2440
  assertSelfTest(!/atris:|fireworks|glm|secret|token/i.test(text), 'doctor output leaked provider/debug text');
2251
2441
  }, results, output);
2252
2442
 
2253
- await runSelfTestCase('chat backend offline preflight', async () => {
2443
+ await runSelfTestCase('chat cloud offline preflight', async () => {
2254
2444
  const previousAuto = process.env.AX_AUTO_LOG;
2255
2445
  process.env.AX_AUTO_LOG = '0';
2256
2446
  const chunks = [];
@@ -2265,7 +2455,6 @@ async function runSelfTest(options = {}) {
2265
2455
  try {
2266
2456
  await chat({
2267
2457
  mode: 'fast',
2268
- route: 'local',
2269
2458
  cwd: os.tmpdir(),
2270
2459
  input: Readable.from(['hi\n', 'exit\n']),
2271
2460
  output: chatOutput,
@@ -2286,8 +2475,8 @@ async function runSelfTest(options = {}) {
2286
2475
  const text = chunks.join('');
2287
2476
  assertSelfTest(healthCalls === 1, 'offline preflight repeated unexpectedly');
2288
2477
  assertSelfTest(/backend\s+offline/.test(text), 'offline backend status missing');
2289
- assertSelfTest(/Local workspace mode needs a running AtrisOS backend:/.test(text), 'local backend hint missing');
2290
- assertSelfTest(!/\/Users\/keshavrao/.test(text), 'local backend hint leaked a developer path');
2478
+ assertSelfTest(/Atris cloud unavailable/.test(text), 'cloud offline guidance missing');
2479
+ assertSelfTest(!/Start backend:|uvicorn main:app/.test(text), 'cloud default showed local backend instructions');
2291
2480
  assertSelfTest(!/secret-token/.test(text), 'offline preflight leaked error detail');
2292
2481
  }, results, output);
2293
2482
 
@@ -2926,6 +3115,24 @@ async function main() {
2926
3115
  .join(' ')
2927
3116
  .trim();
2928
3117
 
3118
+ const missionIntent = missionRunIntentFromMessage(prompt);
3119
+ if (missionIntent) {
3120
+ const result = await runLocalMission(missionIntent, {
3121
+ cwd: process.cwd(),
3122
+ onProgress: (line) => {
3123
+ if (/^pursuing /.test(line)) console.log('Pursuing goal...');
3124
+ else if (/^still_running /.test(line)) {
3125
+ const seconds = Number(String(line).split(/\s+/)[1]);
3126
+ console.log(`Still pursuing... ${formatSeconds(seconds)}`);
3127
+ } else if (/^landing /.test(line)) console.log('Landing goal...');
3128
+ else if (/^\[tick /.test(line)) console.log(line);
3129
+ },
3130
+ });
3131
+ console.log(result.text || 'Mission command failed.');
3132
+ if (!result.ok) process.exit(result.status || 1);
3133
+ return;
3134
+ }
3135
+
2929
3136
  let business = null;
2930
3137
  if (businessSlug) {
2931
3138
  if (normalizeMode(mode) === 'code-fast') {
@@ -2973,7 +3180,7 @@ async function main() {
2973
3180
  console.log('');
2974
3181
  console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
2975
3182
  console.log('');
2976
- console.log(formatRuntimeHealth(await buildRuntimeHealth(), process.stdout));
3183
+ console.log(formatRuntimeHealth(await buildRuntimeHealth({ route: route === 'auto' ? undefined : route }), process.stdout));
2977
3184
  return;
2978
3185
  }
2979
3186
 
@@ -3000,6 +3207,7 @@ async function main() {
3000
3207
 
3001
3208
  console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
3002
3209
  console.log('');
3210
+ maybeWriteAtrisGoalBanner(process.stdout, process.cwd());
3003
3211
  const result = await turnFunctionForMode(mode)(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
3004
3212
  const receipt = receiptFromState(result);
3005
3213
  const approvalRequest = normalizeMode(mode) === 'code-fast' ? null : approvalRequestFromReceipt(receipt);
@@ -3011,8 +3219,7 @@ async function main() {
3011
3219
  console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
3012
3220
  } catch (error) {
3013
3221
  console.error(`x ${error.message}`);
3014
- const shouldShowBackendHint = forceLocal || (prompt && route !== 'cloud' && resolveRoute(prompt, { route: route === 'auto' ? undefined : route }) === 'local');
3015
- if (shouldShowBackendHint) printBackendHint();
3222
+ printBackendHint({ route: route === 'auto' ? undefined : route });
3016
3223
  process.exit(1);
3017
3224
  }
3018
3225
  }
@@ -3082,7 +3289,6 @@ module.exports = {
3082
3289
  renderStreamingMarkdown,
3083
3290
  renderTerminalMarkdown,
3084
3291
  resolveRoute,
3085
- shouldPreflightRuntime,
3086
3292
  runBenchmark,
3087
3293
  runSelfTest,
3088
3294
  runtimeReadyForChat,