atris 3.15.56 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +32 -31
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +45 -83
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +228 -0
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/skill.js +37 -6
  26. package/commands/soul.js +0 -4
  27. package/commands/task.js +5582 -517
  28. package/commands/terminal.js +14 -10
  29. package/commands/wiki.js +87 -1
  30. package/commands/workflow.js +288 -73
  31. package/commands/worktree.js +52 -15
  32. package/commands/xp.js +41 -65
  33. package/lib/auto-accept-certified.js +294 -0
  34. package/lib/file-ops.js +0 -184
  35. package/lib/member-alive.js +232 -0
  36. package/lib/policy-lessons.js +280 -0
  37. package/lib/receipt-evidence.js +64 -0
  38. package/lib/state-detection.js +34 -0
  39. package/lib/task-db.js +568 -16
  40. package/lib/task-proof.js +43 -0
  41. package/package.json +1 -1
  42. package/utils/auth.js +13 -4
  43. package/commands/research.js +0 -52
  44. package/lib/section-merge.js +0 -196
package/ax CHANGED
@@ -15,6 +15,11 @@ const BACKEND = {
15
15
  path: '/api/atris2/turn'
16
16
  };
17
17
  const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
18
+ const CODE_FAST = {
19
+ path: '/api/cursor/turn',
20
+ model: 'composer-2-5-fast',
21
+ publicBase: 'https://api.atris.ai'
22
+ };
18
23
  const CONNECTION_STATUS_PATH = '/api/integrations/status';
19
24
  const CONNECTION_CAPABILITIES_PATH = '/api/atris2/connection-capabilities';
20
25
  const ATRIS2_CONNECTION_STATUS_PATH = '/api/atris2/connection-status';
@@ -53,10 +58,19 @@ const ANSI = {
53
58
  dim: '\x1b[2m',
54
59
  muted: '\x1b[90m',
55
60
  accent: '\x1b[36m',
56
- ok: '\x1b[32m'
61
+ ok: '\x1b[32m',
62
+ magenta: '\x1b[35m'
57
63
  };
58
64
 
65
+ const TIER_COLORS = { fast: '\x1b[32m', pro: '\x1b[36m', max: '\x1b[35m' };
66
+
67
+ function tierColor(mode) {
68
+ return TIER_COLORS[mode] || ANSI.accent;
69
+ }
70
+
59
71
  function modelForMode(mode) {
72
+ if (mode === 'code-fast') return CODE_FAST.model;
73
+ if (mode === 'max') return 'atris:max';
60
74
  return mode === 'fast' ? 'atris:fast' : 'atris:pro';
61
75
  }
62
76
 
@@ -74,34 +88,48 @@ function formatSeconds(totalSeconds) {
74
88
  return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
75
89
  }
76
90
 
77
- function formatHeader({ mode = 'pro', cwd = process.cwd(), chat = false } = {}) {
78
- const label = mode === 'fast' ? 'Atris 2 Fast' : 'Atris 2 Pro';
91
+ function tierLabel(mode) {
92
+ if (mode === 'code-fast') return 'Atris Code Fast';
93
+ if (mode === 'max') return 'Atris 2 Max';
94
+ if (mode === 'fast') return 'Atris 2 Fast';
95
+ return 'Atris 2 Pro';
96
+ }
97
+
98
+ function formatHeader({ mode = 'pro', cwd = process.cwd(), chat = false } = {}, options = {}) {
79
99
  return [
80
- `${label}${chat ? ' chat' : ''} (${modelForMode(mode)})`,
100
+ paint(`${tierLabel(mode)}${chat ? ' chat' : ''}`, [ANSI.bold, tierColor(normalizeMode(mode))], options),
81
101
  cwd,
82
- chat ? 'exit with exit, quit, or :q' : '',
102
+ chat ? paint('type / for the menu · /fast /pro /max swap tiers · exit to leave', [ANSI.muted], options) : '',
83
103
  ].filter(Boolean).join('\n');
84
104
  }
85
105
 
86
106
  function formatUsage() {
87
107
  return [
88
- 'ax - Atris 2 local coding agent',
108
+ 'ax - Atris local/code agent',
89
109
  '',
90
110
  'Usage:',
91
- ' ax [--pro|--fast] [--local|--cloud] <message>',
92
- ' ax [--pro|--fast] [--local|--cloud] --chat',
93
- ' ax [--pro|--fast] --doctor',
94
- ' ax [--fast] --benchmark',
111
+ ' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] <message>',
112
+ ' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] --chat',
113
+ ' ax [--max|--pro|--fast] --business <slug> [<message>|--chat]',
114
+ ' ax [--max|--pro|--fast|--code-fast] --doctor',
115
+ ' ax [--max|--fast] --benchmark',
95
116
  '',
96
117
  'Modes:',
118
+ ' --max local workspace agent, highest reasoning, slowest turns',
97
119
  ' --pro local workspace agent, deeper tool loop',
98
120
  ' --fast local workspace agent, faster low-latency turns',
121
+ ' --code-fast Atris Code Fast public lane',
99
122
  ' --local force local workspace tools',
100
123
  ' --cloud force authenticated cloud connectors/chat',
124
+ ' --business <slug> run tools on that business cloud workspace (EC2)',
125
+ ' --verify <cmd> gate the turn on this command passing (default: no verifier)',
101
126
  '',
102
127
  'Examples:',
103
128
  ' ax --pro find the config file and explain it',
104
129
  ' ax --fast what files are here',
130
+ ' ax --max refactor this module and verify the tests',
131
+ ' ax --max --verify "npm test" fix the failing suite',
132
+ ' ax --code-fast explain this error',
105
133
  ' ax --fast what is on my calendar today',
106
134
  ' ax --pro --chat',
107
135
  ].join('\n');
@@ -112,7 +140,9 @@ function timestampForFile(date = new Date()) {
112
140
  }
113
141
 
114
142
  function stripAnsi(value) {
115
- return String(value || '').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
143
+ return String(value || '')
144
+ .replace(/\x1b\]8;;[^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
145
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
116
146
  }
117
147
 
118
148
  function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', output = process.stdout } = {}) {
@@ -170,9 +200,48 @@ function backendPathUrl(pathname) {
170
200
  return new URL(pathname, backendBaseUrl()).toString();
171
201
  }
172
202
 
203
+ function codeFastBaseUrl() {
204
+ return (process.env.AX_CODE_FAST_BACKEND_URL
205
+ || process.env.AX_CODE_FAST_API_BASE
206
+ || process.env.ATRIS_API_BASE
207
+ || CODE_FAST.publicBase).replace(/\/$/, '');
208
+ }
209
+
210
+ function codeFastUrl() {
211
+ return new URL(CODE_FAST.path, codeFastBaseUrl()).toString();
212
+ }
213
+
214
+ function isLoopbackUrl(value) {
215
+ try {
216
+ const parsed = new URL(value);
217
+ return ['127.0.0.1', 'localhost', '::1'].includes(parsed.hostname);
218
+ } catch (_) {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ function isCodeFastLocal(options = {}) {
224
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
225
+ return route === 'local' && isLoopbackUrl(options.codeFastBaseUrl || codeFastBaseUrl());
226
+ }
227
+
173
228
  function buildRunProfile(options = {}) {
174
- const mode = options.mode === 'fast' ? 'fast' : 'pro';
229
+ const mode = normalizeMode(options.mode);
175
230
  const cwd = options.cwd || process.cwd();
231
+ if (mode === 'code-fast') {
232
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
233
+ return {
234
+ endpoint: codeFastUrl(),
235
+ mode,
236
+ route,
237
+ model: CODE_FAST.model,
238
+ workspace_path: isCodeFastLocal(options) ? cwd : 'cloud scratch',
239
+ max_turns: 1,
240
+ streaming: false,
241
+ runtime: isCodeFastLocal(options) ? 'local Cursor SDK through backend' : 'authenticated Code Fast cloud scratch',
242
+ reasoning: 'Composer 2.5 fast lane; charges 10 credits per public turn'
243
+ };
244
+ }
176
245
  const route = resolveRoute(options.message || 'doctor', options);
177
246
  const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
178
247
  return {
@@ -184,15 +253,17 @@ function buildRunProfile(options = {}) {
184
253
  max_turns: payload.max_turns,
185
254
  streaming: true,
186
255
  runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
187
- reasoning: mode === 'pro'
188
- ? 'backend reports run row; Pro workspace tool loop uses API default medium'
189
- : 'backend reports run row; Fast workspace tool loop uses provider default'
256
+ reasoning: mode === 'max'
257
+ ? 'backend reports run row; Max workspace tool loop uses high reasoning effort'
258
+ : mode === 'pro'
259
+ ? 'backend reports run row; Pro workspace tool loop uses API default medium'
260
+ : 'backend reports run row; Fast workspace tool loop uses provider default'
190
261
  };
191
262
  }
192
263
 
193
264
  function formatRunProfile(profile, options = {}) {
194
265
  const rows = [
195
- ['mode', `${profile.mode} (${profile.model})`],
266
+ ['mode', profile.mode],
196
267
  ['endpoint', profile.endpoint],
197
268
  ['route', profile.route || 'auto'],
198
269
  ['workspace', formatPathSubject(profile.workspace_path, options)],
@@ -428,17 +499,75 @@ function resolveRoute(message, options = {}) {
428
499
  return 'local';
429
500
  }
430
501
 
431
- function formatPrompt() {
432
- return ' ';
502
+ function normalizeMode(mode) {
503
+ if (mode === 'code-fast' || mode === 'code') return 'code-fast';
504
+ if (mode === 'max') return 'max';
505
+ return mode === 'fast' ? 'fast' : 'pro';
433
506
  }
434
507
 
435
- function formatWorkingLine(ms) {
508
+ function formatPrompt(mode, options = {}) {
509
+ if (!mode) return '› ';
510
+ const tier = normalizeMode(mode);
511
+ return `${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `;
512
+ }
513
+
514
+ const TIER_COMMANDS = new Map([
515
+ ['/fast', 'fast'],
516
+ ['/pro', 'pro'],
517
+ ['/max', 'max'],
518
+ ]);
519
+
520
+ function chatTierCommand(line) {
521
+ return TIER_COMMANDS.get(String(line || '').trim().toLowerCase()) || null;
522
+ }
523
+
524
+ const CHAT_COMMANDS = [
525
+ ['/fast', 'quick answers, lowest latency'],
526
+ ['/pro', 'deeper tool loop for real work'],
527
+ ['/max', 'highest reasoning for the hardest jobs'],
528
+ ['/help', 'show this menu'],
529
+ ['exit', 'leave chat'],
530
+ ];
531
+
532
+ function chatMenu(options = {}) {
533
+ return CHAT_COMMANDS
534
+ .map(([name, desc]) => {
535
+ const tier = TIER_COMMANDS.get(name);
536
+ const color = tier ? tierColor(tier) : ANSI.accent;
537
+ return ` ${paint(name.padEnd(6), [color], options)} ${paint(desc, [ANSI.muted], options)}`;
538
+ })
539
+ .join('\n');
540
+ }
541
+
542
+ function chatCompleter(line) {
543
+ const trimmed = String(line || '').trimStart().toLowerCase();
544
+ if (!trimmed.startsWith('/')) return [[], line];
545
+ const names = CHAT_COMMANDS.map(([name]) => name).filter(name => name.startsWith('/'));
546
+ const hits = names.filter(name => name.startsWith(trimmed));
547
+ return [hits.length ? hits : names, trimmed];
548
+ }
549
+
550
+ function formatWorkingLine(ms, verb, frame) {
436
551
  const totalSeconds = Math.max(1, Math.round((Number(ms) || 0) / 1000));
552
+ if (verb) return `${frame || '•'} ${verb}… (${formatSeconds(totalSeconds)} · ctrl-c to interrupt)`;
437
553
  return `• Working (${formatSeconds(totalSeconds)} • ctrl-c to interrupt)`;
438
554
  }
439
555
 
440
- function formatDoneLine(ms) {
441
- return `— Worked for ${formatDuration(ms)} —`;
556
+ function verbForTool(tool) {
557
+ const name = String(tool || '').toLowerCase();
558
+ if (name.includes('read')) return 'Reading';
559
+ if (name.includes('grep') || name.includes('glob') || name.includes('search')) return 'Searching';
560
+ if (name.includes('bash') || name.includes('command') || name.includes('run')) return 'Running';
561
+ if (name.includes('write') || name.includes('edit')) return 'Editing';
562
+ return 'Working';
563
+ }
564
+
565
+ function formatDoneLine(ms, credits) {
566
+ const base = `— Worked for ${formatDuration(ms)}`;
567
+ if (Number.isFinite(credits) && credits > 0) {
568
+ return `${base} · ${credits} ${credits === 1 ? 'credit' : 'credits'} —`;
569
+ }
570
+ return `${base} —`;
442
571
  }
443
572
 
444
573
  function useColor(options = {}) {
@@ -566,19 +695,29 @@ function truncateMiddle(value, limit = 120) {
566
695
  return `${text.slice(0, head)}...${text.slice(-tail)}`;
567
696
  }
568
697
 
698
+ function hyperlinkPath(text, target, options = {}) {
699
+ if (!useColor(options)) return text;
700
+ const original = String(target || '').trim();
701
+ // Only link things that look like paths; skip prose like "cloud scratch".
702
+ if (!original || /\s/.test(original)) return text;
703
+ const href = `file://${encodeURI(path.resolve(original))}`;
704
+ return `\x1b]8;;${href}\x07${text}\x1b]8;;\x07`;
705
+ }
706
+
569
707
  function formatPathSubject(value, options = {}) {
570
708
  const raw = truncateMiddle(String(value || '').trim(), options.limit || 120);
571
709
  if (!raw) return '';
572
710
  if (raw === '.' || raw === '/' || raw.endsWith('/')) {
573
- return paint(raw, [ANSI.accent], options);
711
+ return hyperlinkPath(paint(raw, [ANSI.accent], options), value, options);
574
712
  }
575
713
 
576
714
  const slash = raw.lastIndexOf('/');
577
- if (slash === -1) return paint(raw, [ANSI.bold, ANSI.accent], options);
715
+ if (slash === -1) return hyperlinkPath(paint(raw, [ANSI.bold, ANSI.accent], options), value, options);
578
716
 
579
717
  const parent = raw.slice(0, slash + 1);
580
718
  const filename = raw.slice(slash + 1);
581
- return `${paint(parent, [ANSI.muted], options)} ${paint(filename, [ANSI.bold, ANSI.accent], options)}`;
719
+ const text = `${paint(parent, [ANSI.muted], options)} ${paint(filename, [ANSI.bold, ANSI.accent], options)}`;
720
+ return hyperlinkPath(text, value, options);
582
721
  }
583
722
 
584
723
  function formatAuxRow(label, value, options = {}) {
@@ -591,6 +730,9 @@ function createProgressReporter(output, options = {}) {
591
730
  const enabled = options.showProgress !== false;
592
731
  const isTty = Boolean(output && output.isTTY);
593
732
  const startedAt = Date.now();
733
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
734
+ let frameIndex = 0;
735
+ let verb = 'Working';
594
736
  let interval = null;
595
737
  let timeout = null;
596
738
  let shown = false;
@@ -598,11 +740,11 @@ function createProgressReporter(output, options = {}) {
598
740
 
599
741
  const render = () => {
600
742
  if (!enabled || closed) return;
601
- const line = formatWorkingLine(Date.now() - startedAt);
602
743
  if (isTty) {
603
- output.write(`\r${line}\x1b[K`);
744
+ const frame = paint(frames[frameIndex++ % frames.length], [ANSI.accent], output);
745
+ output.write(`\r${formatWorkingLine(Date.now() - startedAt, verb, frame)}\x1b[K`);
604
746
  } else if (!shown) {
605
- output.write(`${line}\n`);
747
+ output.write(`${formatWorkingLine(Date.now() - startedAt)}\n`);
606
748
  }
607
749
  shown = true;
608
750
  };
@@ -612,9 +754,12 @@ function createProgressReporter(output, options = {}) {
612
754
  if (!enabled) return;
613
755
  timeout = setTimeout(() => {
614
756
  render();
615
- if (isTty) interval = setInterval(render, 1000);
757
+ if (isTty) interval = setInterval(render, 120);
616
758
  }, 700);
617
759
  },
760
+ setVerb(next) {
761
+ verb = next || 'Working';
762
+ },
618
763
  clear() {
619
764
  if (!isTty || !shown || closed) return;
620
765
  output.write('\r\x1b[K');
@@ -642,22 +787,34 @@ function parseSseBlock(block) {
642
787
  return JSON.parse(data);
643
788
  }
644
789
 
645
- function summarizeToolInput(block) {
646
- const options = arguments[1] || {};
647
- const tool = block.tool || 'tool';
790
+ function summarizeToolArgs(block, options = {}) {
648
791
  const input = block.input || {};
649
- const toolName = paint(tool, [ANSI.bold], options);
650
792
  const pathValue = input.file_path || input.path;
651
- if (input.command) return `${toolName} ${truncateMiddle(input.command, 120)}`;
793
+ if (input.command) return truncateMiddle(input.command, 120);
652
794
  if (input.pattern || input.query) {
653
795
  const pattern = truncateMiddle(input.pattern || input.query, 80);
654
- return pathValue
655
- ? `${toolName} ${pattern} in ${formatPathSubject(pathValue, options)}`
656
- : `${toolName} ${pattern}`;
796
+ return pathValue ? `${pattern} in ${formatPathSubject(pathValue, options)}` : pattern;
657
797
  }
658
- if (pathValue) return `${toolName} ${formatPathSubject(pathValue, options)}`;
798
+ if (pathValue) return formatPathSubject(pathValue, options);
659
799
  const subject = input.type || '';
660
- return subject ? `${toolName} ${String(subject).slice(0, 120)}` : toolName;
800
+ return subject ? String(subject).slice(0, 120) : '';
801
+ }
802
+
803
+ function summarizeToolInput(block) {
804
+ const options = arguments[1] || {};
805
+ const toolName = paint(block.tool || 'tool', [ANSI.bold], options);
806
+ const args = summarizeToolArgs(block, options);
807
+ return args ? `${toolName} ${args}` : toolName;
808
+ }
809
+
810
+ function formatToolCallLine(block, options = {}) {
811
+ const tool = paint(block.tool || 'tool', [ANSI.bold], options);
812
+ const args = summarizeToolArgs(block, options);
813
+ return `${paint('●', [ANSI.ok], options)} ${tool}(${args})`;
814
+ }
815
+
816
+ function formatToolResultLine(summary, options = {}) {
817
+ return ` ${paint('⎿', [ANSI.muted], options)} ${summary}`;
661
818
  }
662
819
 
663
820
  function summarizeToolResult(result) {
@@ -679,14 +836,24 @@ function summarizeToolResult(result) {
679
836
  const where = parsed.path ? ` in ${formatPathSubject(parsed.path, options)}` : '';
680
837
  return `${parts.join(' / ')}${where}`;
681
838
  }
839
+ if (typeof parsed.content === 'string' && parsed.content) {
840
+ const lines = parsed.content.split('\n').length;
841
+ const where = parsed.path ? ` ${formatPathSubject(parsed.path, options)}` : '';
842
+ return `${lines} ${lines === 1 ? 'line' : 'lines'}${where}`;
843
+ }
682
844
  if (parsed.path && parsed.status) return `${parsed.status} ${formatPathSubject(parsed.path, options)}`;
683
845
  if (parsed.path) return formatPathSubject(parsed.path, options);
684
846
  if (parsed.status) return parsed.status;
685
847
  } catch (_) {
686
- // Bash output and older streams can be plain text.
848
+ // Truncated relay JSON and bash output fall through to the text path.
687
849
  }
688
850
 
689
- return content.length > 120 ? `${content.slice(0, 117)}...` : content;
851
+ const head = content.match(/^\{"status":\s*"(\w+)",\s*"path":\s*"([^"]+)"/);
852
+ if (head) return `${head[1]} ${formatPathSubject(head[2], options)}`;
853
+
854
+ const lines = content.split('\n');
855
+ const first = lines[0].length > 120 ? `${lines[0].slice(0, 117)}...` : lines[0];
856
+ return lines.length > 1 ? `${first} … +${lines.length - 1} lines` : first;
690
857
  }
691
858
 
692
859
  function formatStatusMessage(message) {
@@ -698,10 +865,9 @@ function formatStatusMessage(message) {
698
865
 
699
866
  function formatSystemInit(event, options = {}) {
700
867
  const runtime = event.tool_runtime || {};
701
- const model = runtime.tool_model || runtime.chat_model || event.model || '';
702
868
  const mode = runtime.mode ? String(runtime.mode).replace(/_/g, ' ') : '';
703
869
  const thinking = runtime.reasoning_effort ? `thinking ${runtime.reasoning_effort}` : '';
704
- const parts = [mode || 'runtime', model, thinking].filter(Boolean);
870
+ const parts = [mode || 'runtime', thinking].filter(Boolean);
705
871
  return parts.length ? parts.join(' ') : null;
706
872
  }
707
873
 
@@ -711,6 +877,7 @@ function clearRetriedText(state) {
711
877
  state.wroteText = false;
712
878
  state.lastChar = '\n';
713
879
  state.inAuxBlock = false;
880
+ state.needsBullet = true;
714
881
  resetMarkdownState(state);
715
882
  }
716
883
 
@@ -729,6 +896,10 @@ function flushPendingText(state, output) {
729
896
  if (!state.pendingText && !hasMarkdownRemainder) return;
730
897
  stopProgress(state);
731
898
  if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
899
+ if (state.pendingText && state.needsBullet && output && output.isTTY) {
900
+ output.write('● ');
901
+ state.needsBullet = false;
902
+ }
732
903
  if (state.pendingText) output.write(output && output.isTTY ? renderTerminalMarkdown(state.pendingText, output) : state.pendingText);
733
904
  const flushedMarkdown = hasMarkdownRemainder ? flushStreamingMarkdown(state, output) : '';
734
905
  state.wroteText = true;
@@ -744,6 +915,10 @@ function writeStreamingText(state, output, content) {
744
915
  stopProgress(state);
745
916
  if (state.inAuxBlock && state.lastChar === '\n') output.write('\n');
746
917
  const rendered = renderStreamingMarkdown(state, content, output);
918
+ if (rendered && state.needsBullet && output.isTTY) {
919
+ output.write('● ');
920
+ state.needsBullet = false;
921
+ }
747
922
  if (rendered) output.write(rendered);
748
923
  state.wroteText = true;
749
924
  state.wroteActivity = true;
@@ -753,13 +928,15 @@ function writeStreamingText(state, output, content) {
753
928
 
754
929
  function writeAuxLine(state, output, line) {
755
930
  if (!line) return;
756
- stopProgress(state);
931
+ // Keep the spinner alive between tool lines — just clear it off the row.
932
+ if (state.progress) state.progress.clear();
757
933
  if (state.wroteText && state.lastChar !== '\n') output.write('\n');
758
934
  if (!state.inAuxBlock && state.wroteActivity && state.lastChar === '\n') output.write('\n');
759
935
  output.write(`${line}\n`);
760
936
  state.wroteActivity = true;
761
937
  state.lastChar = '\n';
762
938
  state.inAuxBlock = true;
939
+ state.needsBullet = true;
763
940
  }
764
941
 
765
942
  function compactHistory(history, limit = 8) {
@@ -784,16 +961,23 @@ function buildMessage(message, history = []) {
784
961
  }
785
962
 
786
963
  function buildPayload(message, options = {}) {
787
- const mode = options.mode === 'fast' ? 'fast' : 'pro';
788
- const route = resolveRoute(message, options);
964
+ const mode = normalizeMode(options.mode);
965
+ const route = options.business ? 'local' : resolveRoute(message, options);
789
966
  const local = route !== 'cloud';
967
+ const verifyCommand = String(options.verify || '').trim();
790
968
  const payload = {
791
969
  message: buildMessage(message, options.history || []),
792
970
  model: modelForMode(mode),
793
- max_turns: local ? (mode === 'pro' ? 14 : 8) : 1,
794
- verify_command: 'true'
971
+ max_turns: local ? (mode === 'fast' ? 8 : 14) : 1,
972
+ verify_command: verifyCommand || 'true'
795
973
  };
796
- if (local) {
974
+ if (options.business) {
975
+ // Business cloud workspace: the model loop stays on the backend and every
976
+ // file/bash tool call relays here, executing on the business EC2 via the
977
+ // /terminal endpoint. The workspace_path is a label, not a local path.
978
+ payload.workspace_path = `/workspace/${options.business.slug}`;
979
+ payload.local_executor = true;
980
+ } else if (local) {
797
981
  payload.workspace_path = options.cwd || process.cwd();
798
982
  }
799
983
  if (options.connectionContext) {
@@ -805,18 +989,40 @@ function buildPayload(message, options = {}) {
805
989
  if (!local && connectorWriteIntent(message)) {
806
990
  payload.allow_external_actions = true;
807
991
  payload.cleanup_external_actions = true;
808
- payload.max_turns = mode === 'pro' ? 4 : 2;
992
+ payload.max_turns = mode === 'fast' ? 2 : 4;
993
+ }
994
+ return payload;
995
+ }
996
+
997
+ function buildCodeFastPayload(message, options = {}) {
998
+ const payload = {
999
+ message: buildMessage(message, options.history || []),
1000
+ model: CODE_FAST.model,
1001
+ timeout_seconds: Number(options.timeoutSeconds || 180)
1002
+ };
1003
+ if (isCodeFastLocal(options)) {
1004
+ payload.workspace_path = options.cwd || process.cwd();
809
1005
  }
810
1006
  return payload;
811
1007
  }
812
1008
 
1009
+ function codeFastWorkspaceNotice(result) {
1010
+ const workspace = result && typeof result === 'object' ? result.workspace || {} : {};
1011
+ const billing = result && typeof result === 'object' ? result.billing || {} : {};
1012
+ const mode = workspace.mode || billing.workspace_mode || '';
1013
+ const persistence = workspace.persistence || billing.workspace_persistence || '';
1014
+ if (mode === 'cloud_scratch' || persistence === 'ephemeral') {
1015
+ return 'cloud scratch files are temporary and are not saved to your Mac';
1016
+ }
1017
+ return null;
1018
+ }
1019
+
813
1020
  function handleEvent(event, state, output) {
814
1021
  if (!event || typeof event !== 'object') return;
815
1022
  state.events.push(event);
816
1023
 
817
1024
  if (event.type === 'system_init') {
818
1025
  state.runtime = event;
819
- writeAuxLine(state, output, formatAuxRow('run', formatSystemInit(event, output), output));
820
1026
  return;
821
1027
  }
822
1028
 
@@ -831,7 +1037,10 @@ function handleEvent(event, state, output) {
831
1037
  for (const block of event.blocks) {
832
1038
  if (block && block.type === 'tool_use') {
833
1039
  flushPendingText(state, output);
834
- writeAuxLine(state, output, formatAuxRow('tool', summarizeToolInput(block, output), output));
1040
+ if (state.lastAux === 'result' && state.inAuxBlock) output.write('\n');
1041
+ writeAuxLine(state, output, formatToolCallLine(block, output));
1042
+ state.lastAux = 'call';
1043
+ if (state.progress && state.progress.setVerb) state.progress.setVerb(verbForTool(block.tool));
835
1044
  }
836
1045
  }
837
1046
  return;
@@ -840,18 +1049,41 @@ function handleEvent(event, state, output) {
840
1049
  if (event.type === 'tool_results' && Array.isArray(event.results)) {
841
1050
  for (const result of event.results) {
842
1051
  flushPendingText(state, output);
843
- writeAuxLine(state, output, formatAuxRow('ok', summarizeToolResult(result, output), output));
1052
+ writeAuxLine(state, output, formatToolResultLine(summarizeToolResult(result, output), output));
1053
+ state.lastAux = 'result';
1054
+ }
1055
+ return;
1056
+ }
1057
+
1058
+ if (event.type === 'tool_call_request') {
1059
+ flushPendingText(state, output);
1060
+ const args = event.args || {};
1061
+ const cloudBlock = { tool: `cloud:${args.type || event.name || 'tool'}`, input: { command: args.command, path: args.path } };
1062
+ if (state.lastAux === 'result' && state.inAuxBlock) output.write('\n');
1063
+ writeAuxLine(state, output, formatToolCallLine(cloudBlock, output));
1064
+ state.lastAux = 'call';
1065
+ if (state.progress && state.progress.setVerb) state.progress.setVerb(verbForTool(args.type || event.name));
1066
+ if (state.relay) {
1067
+ // Relayed calls run sequentially: the backend awaits each tool-result
1068
+ // before continuing its loop, so a promise chain preserves order.
1069
+ state.relay.chain = state.relay.chain
1070
+ .then(() => state.relay.execute(event.name, args))
1071
+ .catch((err) => ({ status: 'error', error: String(err.message || err).slice(0, 500) }))
1072
+ .then((result) => state.relay.post(event.call_id, result))
1073
+ .catch((err) => state.errors.push(`tool relay failed: ${err.message}`));
844
1074
  }
845
1075
  return;
846
1076
  }
847
1077
 
848
1078
  if (event.type === 'status' && event.message && event.message !== 'complete') {
849
- if (event.message === 'retrying_with_required_local_tool') {
1079
+ // Any retry status supersedes the draft answer already buffered — drop it
1080
+ // silently instead of printing the abandoned text plus an info row.
1081
+ if (/^retrying/i.test(event.message)) {
850
1082
  clearRetriedText(state);
851
1083
  return;
852
1084
  }
853
1085
  flushPendingText(state, output);
854
- writeAuxLine(state, output, formatAuxRow('info', formatStatusMessage(event.message), output));
1086
+ writeAuxLine(state, output, paint( ${formatStatusMessage(event.message)}`, [ANSI.muted], output));
855
1087
  return;
856
1088
  }
857
1089
 
@@ -869,7 +1101,7 @@ function handleEvent(event, state, output) {
869
1101
  }
870
1102
 
871
1103
  async function postTurn(message, options = {}) {
872
- const route = resolveRoute(message, options);
1104
+ const route = options.business ? 'local' : resolveRoute(message, options);
873
1105
  const local = route !== 'cloud';
874
1106
  const token = authToken();
875
1107
  const shouldSendConnectionContext = options.connectionContext
@@ -883,7 +1115,10 @@ async function postTurn(message, options = {}) {
883
1115
  const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
884
1116
  const postData = JSON.stringify(payload);
885
1117
  const output = options.output || process.stdout;
886
- const timeoutMs = payload.model === 'atris:pro' ? 180000 : 60000;
1118
+ // Relayed business turns wait on EC2 terminal calls (up to 60s each) with no
1119
+ // SSE traffic in between, so the socket-idle timeout needs more headroom.
1120
+ const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1121
+ const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
887
1122
  const turnUrl = new URL(backendUrl());
888
1123
  const transport = turnUrl.protocol === 'https:' ? https : http;
889
1124
  const state = {
@@ -897,9 +1132,16 @@ async function postTurn(message, options = {}) {
897
1132
  lastChar: '\n',
898
1133
  progress: null,
899
1134
  inAuxBlock: false,
1135
+ needsBullet: true,
1136
+ lastAux: '',
900
1137
  markdownMode: 'normal',
901
1138
  markdownBuffer: '',
902
- markdownCarry: ''
1139
+ markdownCarry: '',
1140
+ relay: options.business ? {
1141
+ chain: Promise.resolve(),
1142
+ execute: options.business.executor,
1143
+ post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl())
1144
+ } : null
903
1145
  };
904
1146
 
905
1147
  return new Promise((resolve, reject) => {
@@ -993,8 +1235,121 @@ async function postTurn(message, options = {}) {
993
1235
  });
994
1236
  }
995
1237
 
1238
+ async function postCodeFastTurn(message, options = {}) {
1239
+ const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
1240
+ if (route === 'local' && !isCodeFastLocal(options)) {
1241
+ throw new Error('Atris Code Fast local workspace requires AX_CODE_FAST_BACKEND_URL=http://127.0.0.1:8000; use --cloud for the public lane.');
1242
+ }
1243
+
1244
+ const payload = buildCodeFastPayload(message, { ...options, route });
1245
+ const postData = JSON.stringify(payload);
1246
+ const output = options.output || process.stdout;
1247
+ const token = authToken();
1248
+ const timeoutMs = Math.max(10000, Math.min(600000, Number(payload.timeout_seconds || 180) * 1000));
1249
+ const turnUrl = new URL(codeFastUrl());
1250
+ const transport = turnUrl.protocol === 'https:' ? https : http;
1251
+ const state = {
1252
+ events: [],
1253
+ errors: [],
1254
+ output: '',
1255
+ pendingText: '',
1256
+ wroteText: false,
1257
+ wroteActivity: false,
1258
+ durationMs: 0,
1259
+ lastChar: '\n',
1260
+ progress: null,
1261
+ inAuxBlock: false,
1262
+ needsBullet: true,
1263
+ lastAux: '',
1264
+ markdownMode: 'normal',
1265
+ markdownBuffer: '',
1266
+ markdownCarry: ''
1267
+ };
1268
+
1269
+ return new Promise((resolve, reject) => {
1270
+ let settled = false;
1271
+ const startedAt = Date.now();
1272
+ state.progress = createProgressReporter(output, options);
1273
+ state.progress.start();
1274
+
1275
+ const finish = (error, value) => {
1276
+ if (settled) return;
1277
+ settled = true;
1278
+ if (state.progress) state.progress.stop();
1279
+ state.progress = null;
1280
+ state.durationMs = Date.now() - startedAt;
1281
+ if (error) reject(error);
1282
+ else resolve(value);
1283
+ };
1284
+
1285
+ const req = transport.request({
1286
+ hostname: turnUrl.hostname,
1287
+ port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
1288
+ path: `${turnUrl.pathname}${turnUrl.search}`,
1289
+ method: 'POST',
1290
+ headers: {
1291
+ 'Content-Type': 'application/json',
1292
+ 'Content-Length': Buffer.byteLength(postData),
1293
+ Accept: 'application/json',
1294
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
1295
+ }
1296
+ }, (res) => {
1297
+ const chunks = [];
1298
+ res.on('data', chunk => chunks.push(chunk));
1299
+ res.on('end', () => {
1300
+ const rawBody = Buffer.concat(chunks).toString('utf8');
1301
+ let parsed = null;
1302
+ try {
1303
+ parsed = rawBody ? JSON.parse(rawBody) : null;
1304
+ } catch (_) {
1305
+ parsed = null;
1306
+ }
1307
+
1308
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1309
+ const detail = parsed && (parsed.detail || parsed.error || parsed.message);
1310
+ finish(new Error(detail || rawBody || `HTTP ${res.statusCode}`));
1311
+ return;
1312
+ }
1313
+
1314
+ const text = String((parsed && (parsed.text || parsed.result || parsed.message)) || '').trim();
1315
+ if (text) {
1316
+ state.output = text;
1317
+ if (output && output.isTTY) output.write(renderTerminalMarkdown(text, output));
1318
+ else output.write(text);
1319
+ state.wroteText = true;
1320
+ state.wroteActivity = true;
1321
+ state.lastChar = text.slice(-1);
1322
+ }
1323
+ const notice = codeFastWorkspaceNotice(parsed);
1324
+ if (notice) {
1325
+ if (state.lastChar && state.lastChar !== '\n') output.write('\n');
1326
+ output.write(`${formatAuxRow('note', notice, output)}\n`);
1327
+ state.wroteActivity = true;
1328
+ state.lastChar = '\n';
1329
+ }
1330
+ state.events.push({ type: 'result', result: text, raw: parsed });
1331
+ state.billing = parsed && parsed.billing ? parsed.billing : null;
1332
+ if (parsed && parsed.duration_ms) state.durationMs = parsed.duration_ms;
1333
+ finish(null, state);
1334
+ });
1335
+ });
1336
+
1337
+ req.on('error', finish);
1338
+ req.setTimeout(timeoutMs, () => {
1339
+ finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
1340
+ req.destroy();
1341
+ });
1342
+ req.write(postData);
1343
+ req.end();
1344
+ });
1345
+ }
1346
+
1347
+ function turnFunctionForMode(mode) {
1348
+ return normalizeMode(mode) === 'code-fast' ? postCodeFastTurn : postTurn;
1349
+ }
1350
+
996
1351
  async function chat(options = {}) {
997
- const mode = options.mode === 'fast' ? 'fast' : 'pro';
1352
+ let mode = normalizeMode(options.mode);
998
1353
  const cwd = options.cwd || process.cwd();
999
1354
  const input = options.input || process.stdin;
1000
1355
  const baseOutput = options.output || process.stdout;
@@ -1002,7 +1357,7 @@ async function chat(options = {}) {
1002
1357
  const output = logger ? logger.output : baseOutput;
1003
1358
  const history = [];
1004
1359
 
1005
- output.write(`${formatHeader({ mode, cwd, chat: true })}\n\n`);
1360
+ output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
1006
1361
  if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
1007
1362
 
1008
1363
  const runLine = async (line) => {
@@ -1010,11 +1365,29 @@ async function chat(options = {}) {
1010
1365
  if (!trimmed) return false;
1011
1366
  if (EXIT_WORDS.has(trimmed.toLowerCase())) return true;
1012
1367
 
1368
+ if (trimmed === '/' || trimmed.toLowerCase() === '/help') {
1369
+ output.write(`${chatMenu(output)}\n\n`);
1370
+ return false;
1371
+ }
1372
+
1373
+ const tier = chatTierCommand(trimmed);
1374
+ if (tier) {
1375
+ mode = tier;
1376
+ if (logger) logger.write(`${trimmed}\n`);
1377
+ output.write(`${paint(`· ${tierLabel(mode)}`, [tierColor(mode)], output)}\n\n`);
1378
+ return false;
1379
+ }
1380
+
1381
+ if (trimmed.startsWith('/')) {
1382
+ output.write(`${chatMenu(output)}\n\n`);
1383
+ return false;
1384
+ }
1385
+
1013
1386
  if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
1014
1387
  output.write('\n');
1015
- const result = await postTurn(trimmed, { mode, cwd, history, output });
1388
+ const result = await turnFunctionForMode(mode)(trimmed, { mode, cwd, history, output, route: options.route, business: options.business, verify: options.verify });
1016
1389
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
1017
- output.write(`${formatDoneLine(result.durationMs)}\n\n`);
1390
+ output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n\n`);
1018
1391
  history.push({ role: 'user', content: trimmed });
1019
1392
  history.push({ role: 'assistant', content: result.output || '' });
1020
1393
  return false;
@@ -1029,8 +1402,8 @@ async function chat(options = {}) {
1029
1402
  return;
1030
1403
  }
1031
1404
 
1032
- const rl = readline.createInterface({ input, output: baseOutput });
1033
- const ask = () => new Promise(resolve => rl.question(formatPrompt(mode), resolve));
1405
+ const rl = readline.createInterface({ input, output: baseOutput, completer: chatCompleter });
1406
+ const ask = () => new Promise(resolve => rl.question(formatPrompt(mode, baseOutput), resolve));
1034
1407
  while (true) {
1035
1408
  const line = await ask();
1036
1409
  if (await runLine(line)) {
@@ -1066,6 +1439,14 @@ function receiptFromState(state) {
1066
1439
  return event ? event.receipt : null;
1067
1440
  }
1068
1441
 
1442
+ function creditsFromState(state) {
1443
+ const receipt = receiptFromState(state);
1444
+ const billing = receipt && receipt.billing;
1445
+ if (!billing || billing.billed === false) return null;
1446
+ const credits = Number(billing.credits_charged);
1447
+ return Number.isFinite(credits) && credits > 0 ? credits : null;
1448
+ }
1449
+
1069
1450
  function toolEventsFromState(state) {
1070
1451
  const receipt = receiptFromState(state);
1071
1452
  if (receipt && Array.isArray(receipt.tool_events)) return receipt.tool_events;
@@ -1096,13 +1477,14 @@ async function runBenchmarkCase(label, fn, results, output) {
1096
1477
  }
1097
1478
 
1098
1479
  async function runBenchmark(options = {}) {
1099
- const mode = options.mode === 'pro' ? 'pro' : 'fast';
1480
+ const normalized = normalizeMode(options.mode);
1481
+ const mode = normalized === 'code-fast' ? 'fast' : normalized;
1100
1482
  const output = options.output || process.stdout;
1101
1483
  const results = [];
1102
1484
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ax-atris2-benchmark-'));
1103
1485
  const localOut = () => bufferedOutput();
1104
1486
 
1105
- output.write(`Atris 2 ${mode === 'fast' ? 'Fast' : 'Pro'} benchmark\n`);
1487
+ output.write(`Atris 2 ${mode === 'fast' ? 'Fast' : mode === 'max' ? 'Max' : 'Pro'} benchmark\n`);
1106
1488
  output.write(`${backendUrl()}\n\n`);
1107
1489
 
1108
1490
  try {
@@ -1221,17 +1603,74 @@ async function main() {
1221
1603
  return;
1222
1604
  }
1223
1605
 
1224
- const mode = args.includes('--fast') ? 'fast' : 'pro';
1606
+ // --business <slug>: chat against that business's cloud workspace. The flag
1607
+ // takes a value, so splice the pair out before building the prompt.
1608
+ let businessSlug = null;
1609
+ const bizIdx = args.indexOf('--business');
1610
+ if (bizIdx !== -1) {
1611
+ businessSlug = String(args[bizIdx + 1] || '').toLowerCase();
1612
+ if (!/^[a-z0-9-]+$/.test(businessSlug)) {
1613
+ console.error('Usage: ax --business <slug> [prompt]');
1614
+ process.exit(1);
1615
+ }
1616
+ args.splice(bizIdx, 2);
1617
+ }
1618
+
1619
+ // --verify <cmd>: opt-in turn verifier. The flag takes a value, so splice
1620
+ // the pair out before building the prompt.
1621
+ let verify = '';
1622
+ const verifyIdx = args.indexOf('--verify');
1623
+ if (verifyIdx !== -1) {
1624
+ verify = String(args[verifyIdx + 1] || '').trim();
1625
+ if (!verify) {
1626
+ console.error('Usage: ax --verify "<cmd>" [prompt]');
1627
+ process.exit(1);
1628
+ }
1629
+ args.splice(verifyIdx, 2);
1630
+ }
1631
+
1632
+ const mode = args.includes('--code-fast') || args.includes('--code') ? 'code-fast' : args.includes('--max') ? 'max' : args.includes('--fast') ? 'fast' : 'pro';
1225
1633
  const doctor = args.includes('--doctor');
1226
1634
  const benchmark = args.includes('--benchmark');
1227
1635
  const forceCloud = args.includes('--cloud');
1228
1636
  const forceLocal = args.includes('--local');
1229
1637
  const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
1230
1638
  const prompt = args
1231
- .filter(arg => !['--fast', '--pro', '--chat', '--doctor', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
1639
+ .filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
1232
1640
  .join(' ')
1233
1641
  .trim();
1234
1642
 
1643
+ let business = null;
1644
+ if (businessSlug) {
1645
+ if (normalizeMode(mode) === 'code-fast') {
1646
+ console.error('--business is not supported with --code-fast');
1647
+ process.exit(1);
1648
+ }
1649
+ const creds = loadCredentials();
1650
+ if (!creds || !creds.token) {
1651
+ console.error('Not logged in. Run: atris login');
1652
+ process.exit(1);
1653
+ }
1654
+ const { resolveBusiness, ensureAwake } = require('./commands/terminal');
1655
+ const { makeCloudExecutor, postToolResult } = require('./commands/workflow');
1656
+ const biz = await resolveBusiness(creds.token, businessSlug);
1657
+ if (!biz || !biz.workspaceId) {
1658
+ console.error(`Business "${businessSlug}" not found or has no workspace.`);
1659
+ process.exit(1);
1660
+ }
1661
+ const awake = await ensureAwake(creds.token, biz.businessId);
1662
+ if (!awake) {
1663
+ console.error('Business computer did not become ready in time.');
1664
+ process.exit(1);
1665
+ }
1666
+ business = {
1667
+ slug: businessSlug,
1668
+ executor: makeCloudExecutor({ token: creds.token, businessId: biz.businessId, workspaceId: biz.workspaceId, slug: businessSlug }),
1669
+ postToolResult
1670
+ };
1671
+ console.log(`cloud workspace: ${biz.businessName || businessSlug}`);
1672
+ }
1673
+
1235
1674
  try {
1236
1675
  if (benchmark) {
1237
1676
  await runBenchmark({ mode, cwd: process.cwd(), output: process.stdout });
@@ -1239,22 +1678,22 @@ async function main() {
1239
1678
  }
1240
1679
 
1241
1680
  if (doctor) {
1242
- console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
1681
+ console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
1243
1682
  console.log('');
1244
1683
  console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
1245
1684
  return;
1246
1685
  }
1247
1686
 
1248
1687
  if (!prompt || args.includes('--chat')) {
1249
- await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
1688
+ await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
1250
1689
  return;
1251
1690
  }
1252
1691
 
1253
- console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }));
1692
+ console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
1254
1693
  console.log('');
1255
- const result = await postTurn(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route });
1694
+ const result = await turnFunctionForMode(mode)(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
1256
1695
  console.log('');
1257
- console.log(formatDoneLine(result.durationMs));
1696
+ console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
1258
1697
  } catch (error) {
1259
1698
  console.error(`x ${error.message}`);
1260
1699
  printBackendHint();
@@ -1271,11 +1710,19 @@ module.exports = {
1271
1710
  authUserId,
1272
1711
  backendBaseUrl,
1273
1712
  backendUrl,
1713
+ buildCodeFastPayload,
1274
1714
  buildPayload,
1275
1715
  cachedIntegrationStatus,
1276
1716
  buildConnectionContext,
1277
1717
  buildRunProfile,
1278
1718
  chat,
1719
+ chatCompleter,
1720
+ chatMenu,
1721
+ chatTierCommand,
1722
+ codeFastWorkspaceNotice,
1723
+ creditsFromState,
1724
+ codeFastBaseUrl,
1725
+ codeFastUrl,
1279
1726
  createRunLogger,
1280
1727
  createProgressReporter,
1281
1728
  formatDoneLine,
@@ -1291,11 +1738,13 @@ module.exports = {
1291
1738
  handleEvent,
1292
1739
  modelForMode,
1293
1740
  parseSseBlock,
1741
+ postCodeFastTurn,
1294
1742
  postTurn,
1295
1743
  renderStreamingMarkdown,
1296
1744
  renderTerminalMarkdown,
1297
1745
  resolveRoute,
1298
1746
  runBenchmark,
1299
1747
  summarizeToolInput,
1300
- summarizeToolResult
1748
+ summarizeToolResult,
1749
+ tierLabel
1301
1750
  };