atris 3.30.8 → 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;
@@ -598,15 +612,7 @@ function connectorWriteIntent(message) {
598
612
  }
599
613
 
600
614
  function workspaceIntent(message) {
601
- return Boolean(workspaceIntentReason(message));
602
- }
603
-
604
- function workspaceIntentReason(message) {
605
- const text = String(message || '');
606
- if (/\b(files?|folders?|repo|workspace|project|directory|tree|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map|tests?)\b/i.test(text)) return 'prompt mentions workspace, file, code, or repo terms';
607
- if (/(^|\s|["'`])(?:\.{0,2}\/)?[\w.-]+\/[\w./-]+|[\w.-]+\.(?:js|jsx|ts|tsx|mjs|cjs|json|md|py|rb|go|rs|java|css|scss|html|yml|yaml|toml|env|txt)\b/i.test(text)) return 'prompt names a file path or filename';
608
- if (/\b(read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|build|refactor)\b.*\b(this|file|folder|repo|workspace|project|directory|module|component|package|readme|source|code|branch|suite|build|bug)\b/i.test(text)) return 'prompt asks to operate on workspace material';
609
- return '';
615
+ return /\b(files?|folders?|repo|workspace|project|directory|tree|read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|tests?|build|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map)\b/i.test(message || '');
610
616
  }
611
617
 
612
618
  function githubWorkspaceIntent(message) {
@@ -616,22 +622,9 @@ function githubWorkspaceIntent(message) {
616
622
  }
617
623
 
618
624
  function resolveRoute(message, options = {}) {
619
- return routeDecision(message, options).route;
620
- }
621
-
622
- function routeDecision(message, options = {}) {
623
- if (options.route === 'local' || options.forceLocal) return { route: 'local', reason: 'explicit --local' };
624
- if (options.route === 'cloud' || options.forceCloud) return { route: 'cloud', reason: 'explicit --cloud' };
625
- if (githubWorkspaceIntent(message)) return { route: 'local', reason: 'prompt asks for GitHub repo mutation' };
626
- const reason = workspaceIntentReason(message);
627
- if (mentionsConnector(message) && !reason) return { route: 'cloud', reason: 'connector request uses hosted Atris' };
628
- if (reason) return { route: 'local', reason };
629
- return { route: 'cloud', reason: 'default hosted chat' };
630
- }
631
-
632
- function shouldPreflightRuntime(route, options = {}, mode = 'fast') {
633
- if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') return false;
634
- return route !== 'cloud';
625
+ if (options.route === 'local' || options.forceLocal) return 'local';
626
+ if (options.route === 'cloud' || options.forceCloud) return 'cloud';
627
+ return 'cloud';
635
628
  }
636
629
 
637
630
  function normalizeMode(mode) {
@@ -705,6 +698,161 @@ function formatDoneLine(ms, credits) {
705
698
  return `${base} —`;
706
699
  }
707
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
+
708
856
  function useColor(options = {}) {
709
857
  if (options.color === false) return false;
710
858
  if (process.env.NO_COLOR) return false;
@@ -1345,11 +1493,11 @@ function creditsFromApprovalExecution(response) {
1345
1493
  return Number.isFinite(credits) && credits > 0 ? credits : null;
1346
1494
  }
1347
1495
 
1348
- async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken(), route = 'cloud' } = {}) {
1496
+ async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken() } = {}) {
1349
1497
  return postJson(APPROVAL_EXECUTE_PATH, {
1350
1498
  model,
1351
1499
  approval_request: approvalRequest,
1352
- }, { token, route });
1500
+ }, { token });
1353
1501
  }
1354
1502
 
1355
1503
  async function executeApprovalRequest(approvalRequest, {
@@ -1732,15 +1880,16 @@ function handleEvent(event, state, output) {
1732
1880
  async function postTurn(message, options = {}) {
1733
1881
  const route = options.business ? 'local' : resolveRoute(message, options);
1734
1882
  const local = route !== 'cloud';
1883
+ const endpointRoute = options.business ? 'cloud' : route;
1735
1884
  const token = authToken();
1736
1885
  const shouldSendConnectionContext = options.connectionContext
1737
1886
  || route === 'cloud'
1738
1887
  || mentionsConnector(message)
1739
1888
  || /\b(can you use|can you access|connected|connections?|integrations?|tools?|capabilities)\b/i.test(message || '');
1740
1889
  const connectionContext = options.connectionContext || (shouldSendConnectionContext
1741
- ? await buildConnectionContext({ token, localWorkspace: local, route })
1890
+ ? await buildConnectionContext({ token, localWorkspace: local })
1742
1891
  : null);
1743
- const connectionUserId = !local && isLoopbackBackend({ route }) ? authUserId() : '';
1892
+ const connectionUserId = !local && isLoopbackBackend({ route: endpointRoute }) ? authUserId() : '';
1744
1893
  const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
1745
1894
  const postData = JSON.stringify(payload);
1746
1895
  const output = options.output || process.stdout;
@@ -1748,7 +1897,7 @@ async function postTurn(message, options = {}) {
1748
1897
  // SSE traffic in between, so the socket-idle timeout needs more headroom.
1749
1898
  const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1750
1899
  const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
1751
- const turnUrl = new URL(backendUrl({ route }));
1900
+ const turnUrl = new URL(backendUrl({ route: endpointRoute }));
1752
1901
  const transport = turnUrl.protocol === 'https:' ? https : http;
1753
1902
  const state = {
1754
1903
  events: [],
@@ -1769,7 +1918,7 @@ async function postTurn(message, options = {}) {
1769
1918
  relay: options.business ? {
1770
1919
  chain: Promise.resolve(),
1771
1920
  execute: options.business.executor,
1772
- post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl())
1921
+ post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl({ route: 'cloud' }))
1773
1922
  } : null
1774
1923
  };
1775
1924
 
@@ -1989,23 +2138,27 @@ async function chat(options = {}) {
1989
2138
 
1990
2139
  output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
1991
2140
  if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
1992
-
1993
- const runtimePreflight = { localReady: false };
1994
- const ensureRuntimeReady = async (routeForTurn = 'local', decision = {}) => {
1995
- 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);
1996
2150
  return true;
1997
2151
  }
1998
- if (runtimePreflight.localReady) return true;
1999
2152
  const startedAt = Date.now();
2000
2153
  const health = options.runtimeHealth
2001
- ? await Promise.resolve(options.runtimeHealth({ mode }))
2002
- : 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 });
2003
2156
  if (runtimeReadyForChat(health, mode)) {
2004
- runtimePreflight.localReady = true;
2157
+ runtimePreflight.add(key);
2005
2158
  return true;
2006
2159
  }
2007
2160
  output.write(`${formatRuntimeHealth(health, output)}\n\n`);
2008
- output.write(`${formatBackendHint({ routeReason: decision.reason, explicitLocal: decision.reason === 'explicit --local' })}\n`);
2161
+ output.write(`${formatBackendHint({ route: endpointRoute })}\n`);
2009
2162
  output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
2010
2163
  return false;
2011
2164
  };
@@ -2035,6 +2188,26 @@ async function chat(options = {}) {
2035
2188
 
2036
2189
  if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
2037
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
+
2038
2211
  const pendingApprovalRequest = latestPendingApprovalRequest(history);
2039
2212
  if (pendingApprovalRequest && normalizeMode(mode) !== 'code-fast' && messageCancelsPendingApproval(trimmed)) {
2040
2213
  const startedAt = Date.now();
@@ -2091,11 +2264,10 @@ async function chat(options = {}) {
2091
2264
  }
2092
2265
 
2093
2266
  const pendingTaskPreview = latestPendingTaskPreview(history);
2094
- const decision = pendingTaskPreview
2095
- ? { route: 'cloud', reason: 'pending approval stays on hosted Atris' }
2096
- : routeDecision(trimmed, { route: options.route });
2097
- const route = decision.route;
2098
- if (!(await ensureRuntimeReady(route, decision))) 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;
2099
2271
  const turnFunction = options.turnFunction || turnFunctionForMode(mode);
2100
2272
  const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId });
2101
2273
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
@@ -2158,16 +2330,14 @@ function printBackendHint(options = {}) {
2158
2330
  console.log(formatBackendHint(options));
2159
2331
  }
2160
2332
 
2161
- function backendStartCommand(options = {}) {
2162
- if (options.explicitLocal) return 'Start your AtrisOS backend, or retry with --cloud for hosted chat.';
2163
- return 'Start your AtrisOS backend for local workspace, or retry with --cloud for hosted cloud scratch.';
2333
+ function backendStartCommand() {
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}`;
2164
2335
  }
2165
2336
 
2166
2337
  function formatBackendHint(options = {}) {
2167
- const lines = ['This requires local workspace mode, but no AtrisOS backend is running.'];
2168
- if (options.routeReason && !options.explicitLocal) lines.push(`Routed local because ${options.routeReason}.`);
2169
- lines.push(backendStartCommand(options));
2170
- return lines.join('\n');
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.';
2171
2341
  }
2172
2342
 
2173
2343
  function bufferedOutput() {
@@ -2270,7 +2440,7 @@ async function runSelfTest(options = {}) {
2270
2440
  assertSelfTest(!/atris:|fireworks|glm|secret|token/i.test(text), 'doctor output leaked provider/debug text');
2271
2441
  }, results, output);
2272
2442
 
2273
- await runSelfTestCase('chat backend offline preflight', async () => {
2443
+ await runSelfTestCase('chat cloud offline preflight', async () => {
2274
2444
  const previousAuto = process.env.AX_AUTO_LOG;
2275
2445
  process.env.AX_AUTO_LOG = '0';
2276
2446
  const chunks = [];
@@ -2285,7 +2455,6 @@ async function runSelfTest(options = {}) {
2285
2455
  try {
2286
2456
  await chat({
2287
2457
  mode: 'fast',
2288
- route: 'local',
2289
2458
  cwd: os.tmpdir(),
2290
2459
  input: Readable.from(['hi\n', 'exit\n']),
2291
2460
  output: chatOutput,
@@ -2306,9 +2475,8 @@ async function runSelfTest(options = {}) {
2306
2475
  const text = chunks.join('');
2307
2476
  assertSelfTest(healthCalls === 1, 'offline preflight repeated unexpectedly');
2308
2477
  assertSelfTest(/backend\s+offline/.test(text), 'offline backend status missing');
2309
- assertSelfTest(/This requires local workspace mode, but no AtrisOS backend is running\./.test(text), 'local backend hint missing');
2310
- assertSelfTest(/retry with --cloud for hosted chat/.test(text), 'cloud retry hint missing');
2311
- 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');
2312
2480
  assertSelfTest(!/secret-token/.test(text), 'offline preflight leaked error detail');
2313
2481
  }, results, output);
2314
2482
 
@@ -2947,6 +3115,24 @@ async function main() {
2947
3115
  .join(' ')
2948
3116
  .trim();
2949
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
+
2950
3136
  let business = null;
2951
3137
  if (businessSlug) {
2952
3138
  if (normalizeMode(mode) === 'code-fast') {
@@ -2994,7 +3180,7 @@ async function main() {
2994
3180
  console.log('');
2995
3181
  console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
2996
3182
  console.log('');
2997
- console.log(formatRuntimeHealth(await buildRuntimeHealth(), process.stdout));
3183
+ console.log(formatRuntimeHealth(await buildRuntimeHealth({ route: route === 'auto' ? undefined : route }), process.stdout));
2998
3184
  return;
2999
3185
  }
3000
3186
 
@@ -3021,6 +3207,7 @@ async function main() {
3021
3207
 
3022
3208
  console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
3023
3209
  console.log('');
3210
+ maybeWriteAtrisGoalBanner(process.stdout, process.cwd());
3024
3211
  const result = await turnFunctionForMode(mode)(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
3025
3212
  const receipt = receiptFromState(result);
3026
3213
  const approvalRequest = normalizeMode(mode) === 'code-fast' ? null : approvalRequestFromReceipt(receipt);
@@ -3032,11 +3219,7 @@ async function main() {
3032
3219
  console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
3033
3220
  } catch (error) {
3034
3221
  console.error(`x ${error.message}`);
3035
- const decision = prompt
3036
- ? routeDecision(prompt, { route: route === 'auto' ? undefined : route })
3037
- : { route: forceLocal ? 'local' : 'cloud', reason: forceLocal ? 'explicit --local' : 'default hosted chat' };
3038
- const shouldShowBackendHint = decision.route === 'local';
3039
- if (shouldShowBackendHint) printBackendHint({ routeReason: decision.reason, explicitLocal: forceLocal || decision.reason === 'explicit --local' });
3222
+ printBackendHint({ route: route === 'auto' ? undefined : route });
3040
3223
  process.exit(1);
3041
3224
  }
3042
3225
  }
@@ -3106,7 +3289,6 @@ module.exports = {
3106
3289
  renderStreamingMarkdown,
3107
3290
  renderTerminalMarkdown,
3108
3291
  resolveRoute,
3109
- shouldPreflightRuntime,
3110
3292
  runBenchmark,
3111
3293
  runSelfTest,
3112
3294
  runtimeReadyForChat,