atris 3.30.12 → 3.32.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 (64) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +6 -4
  4. package/atris/CLAUDE.md +8 -0
  5. package/atris/atris.md +51 -19
  6. package/atris/skills/README.md +1 -0
  7. package/atris/skills/blocks/SKILL.md +134 -0
  8. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  9. package/atris/skills/youtube/SKILL.md +31 -11
  10. package/atris.md +15 -0
  11. package/ax +189 -7
  12. package/bin/atris.js +273 -225
  13. package/commands/autoland.js +379 -0
  14. package/commands/autopilot-front.js +273 -0
  15. package/commands/autopilot.js +94 -4
  16. package/commands/business.js +1 -1
  17. package/commands/clean.js +72 -9
  18. package/commands/codex-goal.js +72 -22
  19. package/commands/computer.js +48 -3
  20. package/commands/gm.js +1 -1
  21. package/commands/harvest.js +179 -0
  22. package/commands/init.js +22 -1
  23. package/commands/land.js +442 -0
  24. package/commands/loop-front.js +122 -4
  25. package/commands/member.js +551 -19
  26. package/commands/mission.js +3674 -278
  27. package/commands/play.js +1 -1
  28. package/commands/pulse.js +65 -7
  29. package/commands/run-front.js +144 -0
  30. package/commands/run.js +10 -7
  31. package/commands/sign.js +90 -0
  32. package/commands/strings.js +301 -0
  33. package/commands/task.js +782 -108
  34. package/commands/truth.js +171 -0
  35. package/commands/xp.js +32 -8
  36. package/commands/youtube.js +72 -5
  37. package/decks/README.md +6 -12
  38. package/lib/auto-accept-certified.js +10 -0
  39. package/lib/autoland.js +391 -0
  40. package/lib/context-gatherer.js +0 -8
  41. package/lib/mission-artifact.js +504 -0
  42. package/lib/mission-room.js +846 -0
  43. package/lib/next-moves.js +212 -6
  44. package/lib/operator-next.js +7 -0
  45. package/lib/pulse.js +78 -4
  46. package/lib/runner-command.js +51 -20
  47. package/lib/runs-prune.js +242 -0
  48. package/lib/task-db.js +19 -2
  49. package/lib/task-proof.js +1 -1
  50. package/package.json +4 -4
  51. package/decks/atris-seed-pitch-v3.json +0 -118
  52. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  53. package/decks/atris-seed-pitch-v5.json +0 -109
  54. package/decks/atris-seed-pitch-v6.json +0 -137
  55. package/decks/atris-seed-pitch-v7.json +0 -133
  56. package/decks/mark-pincus-narrative.json +0 -102
  57. package/decks/mark-pincus-sourcery.json +0 -94
  58. package/decks/yash-applied-compute-detailed.json +0 -150
  59. package/decks/yash-applied-compute-generalist.json +0 -82
  60. package/decks/yash-applied-compute-narrative.json +0 -54
  61. package/lib/ax-chat-input.js +0 -164
  62. package/lib/ax-goal.js +0 -307
  63. package/lib/ax-prefs.js +0 -63
  64. package/lib/ax-shimmer.js +0 -63
package/atris.md CHANGED
@@ -49,6 +49,7 @@ Before changing anything, state:
49
49
  Then:
50
50
  - do not execute if another agent owns the same task or files
51
51
  - do not call something complete without verification
52
+ - land or reap: work is not done until it is merged to the base branch or reaped; if you make a branch or worktree, you merge it or delete it before you stop. Run `atris land` to see limbo; anything past 7 days gets salvaged to `.atris/salvage/` and deleted by `atris land --reap`.
52
53
  - do not take irreversible actions without approval from the human
53
54
  - do not hide state outside markdown, logs, diffs, or the journal
54
55
  - do not edit the rules that judge you: the reward config, the authority policy, or this file
@@ -79,6 +80,14 @@ The same discipline for words. Output stays sharp no matter how bloated the cont
79
80
  - **Cut filler.** Drop "it's worth noting", "in order to", "leverage", "seamless", "robust", "delve", stacked hedges, and em dashes. `atris slop` flags the prose tells (em-dash, hype-copy) too.
80
81
  - **Bound verbosity by information, not context.** Say the load-bearing thing and stop. Length tracks what the reader needs to act, nothing more.
81
82
  - **Match the register.** The operator wants the next move; a spec wants the contract; a journal wants one line. Jargon is a lever only when shared: use the reader's precise terms, define a new one once.
83
+ - **Specific AND workable.** Vocabulary carries both: precise enough that another agent can act on it, plain enough that the operator gets it on a phone. A sentence only one of those audiences can use is half-written.
84
+ - **Every queue item earns its surface.** Anything an operator might see (task title, mission objective, roadmap item, tick summary) carries its own why in the same sentence: what it costs to skip, or what it buys. "Agents burn tokens hand-rolling state parsers: add one shared inspect view" surfaces; "novel goal-chain demo" does not. Digests refuse raw titles that can't explain themselves.
85
+ - **Day-one PM test.** Every sentence that reaches an operator must make sense to a PM who joined the company this morning: no internal codenames, no problem-titles standing in for results. If they'd have to ask "what does that mean?", the sentence is unfinished. Detail stays one ask away, always.
86
+ - **Results are capabilities, not test counts.** When work lands, the sentence states what someone can do now that they couldn't before, then what it means for them or the business. Tests are one word ("verified"); the meaning is the sentence. Copy these shapes:
87
+ - "One word now runs the work: `atris autopilot` picks the next mission and keeps going until you say stop. Autonomy stopped being a setup and became a feature."
88
+ - "Missions survive everything: one engine starts the work, another picks it up cold, a third lands it. Work is no longer tied to a chat window, a session, or a vendor."
89
+ - "The system reports in plain English: one daily message with what landed, what waits on you, and who should own what's next. One person can supervise many projects."
90
+ - **Fit the screen.** An operator-facing report shows three results, air between them, and holds the rest on ask. No scrolling: reading it is one glance, and the reader asks for more if they want more.
82
91
 
83
92
  `expected`: this is how an Atris agent writes and builds. Shipping slop or rambling is a failure smell, same as drift or a stale task.
84
93
 
@@ -106,6 +115,12 @@ atris task review <id> --lesson "<what improved>" --next "<next task>"
106
115
 
107
116
  Headless agents should add `--json` where available and read
108
117
  `.atris/state/tasks.projection.json` for a compact board view.
118
+
119
+ Landing policy: when the owner has flipped `atris autoland on`, certified work
120
+ (two independent review passes, real proof, safe verify re-run) lands itself
121
+ with a receipt; agents never run `atris task accept` themselves. Money,
122
+ deploys, security, customer, and outward-facing lanes always wait for the
123
+ human. `atris autoland` shows what lands alone and what waits.
109
124
  Swarlo is the live coordination layer for claims, heartbeats, and reports; the
110
125
  task row/event stream remains the durable source of truth.
111
126
 
package/ax CHANGED
@@ -306,6 +306,20 @@ function formatRunProfile(profile, options = {}) {
306
306
  return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
307
307
  }
308
308
 
309
+ // Auto-routed local turns must never die on a missing local backend — a
310
+ // consumer machine without the launchd service gets a degraded cloud chat
311
+ // turn, not ECONNREFUSED. Probed once per process (REPL lines reuse it).
312
+ // Explicit --local skips the downgrade so devs see the real error. Interim
313
+ // guard until the cloud relay executes workspace tools in-process and
314
+ // consumers stop needing a local backend at all.
315
+ let _localBackendUp = null;
316
+ async function localBackendReachable() {
317
+ if (_localBackendUp !== null) return _localBackendUp;
318
+ const res = await requestJson(ATRIS2_HEALTH_PATH, { timeoutMs: 1200, route: 'local' });
319
+ _localBackendUp = Boolean(res.ok || Number(res.status || 0) > 0);
320
+ return _localBackendUp;
321
+ }
322
+
309
323
  async function buildRuntimeHealth(options = {}) {
310
324
  const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
311
325
  const healthRes = options.healthRes
@@ -621,9 +635,49 @@ function githubWorkspaceIntent(message) {
621
635
  return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
622
636
  }
623
637
 
638
+ // Walk up from cwd looking for a workspace marker (.git or atris/ or atris.md).
639
+ // The process.cwd() answer is cached — resolveRoute runs per chat line — but an
640
+ // explicit startDir (callers pass their own cwd) is always checked fresh.
641
+ let _workspaceCwd = null;
642
+ function insideWorkspaceCwd(startDir) {
643
+ const explicit = typeof startDir === 'string' && startDir.length > 0;
644
+ if (!explicit && _workspaceCwd !== null) return _workspaceCwd;
645
+ let dir = explicit ? startDir : process.cwd();
646
+ let found = false;
647
+ for (let i = 0; i < 24; i += 1) {
648
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'atris.md')) || fs.existsSync(path.join(dir, 'atris'))) {
649
+ found = true;
650
+ break;
651
+ }
652
+ const parent = path.dirname(dir);
653
+ if (parent === dir) break;
654
+ dir = parent;
655
+ }
656
+ if (!explicit) _workspaceCwd = found;
657
+ return found;
658
+ }
659
+
624
660
  function resolveRoute(message, options = {}) {
625
661
  if (options.route === 'local' || options.forceLocal) return 'local';
626
662
  if (options.route === 'cloud' || options.forceCloud) return 'cloud';
663
+ // Workspace standard v2 (2026-07-02, supersedes intent routing): inside a
664
+ // workspace, tools are ALWAYS attached and the model decides whether to use
665
+ // them — the Claude Code / Codex shape. v1 guessed intent with regexes and
666
+ // lost three times in one day ("in scripts/fast-coach/tick.mjs, what does
667
+ // SLOW_MS control?", "what did Atris ship last night?", "find it lol" — all
668
+ // tool-less misses that told the user to paste files they were sitting on).
669
+ // Measured cost of always-tools: pure math 3s vs 2s; and "plan my morning"
670
+ // through the tool lane reads the real task board instead of guessing.
671
+ // Non-workspace cwds stay pure cloud (the privacy line); explicit flags
672
+ // always win. One carve-out: GitHub-connector phrasing prefers the cloud
673
+ // GitHub API lane unless it names local-checkout work.
674
+ const text = String(message || '');
675
+ // Local-checkout work includes every git mutation verb: cloud chat can only
676
+ // read the GitHub connector, so "do a quick github push" through the cloud
677
+ // lane dead-ends in a write-blocked upsell (live miss 2026-07-02). Inside a
678
+ // workspace the local tool loop just runs git.
679
+ if (/\bgithub\b/i.test(text) && !/\b(commits?|branch(?:es)?|checkout|merge|rebase|changes?|diff|push(?:es|ed|ing)?|pull(?:s|ed|ing)?|fetch|clone|stash|tags?)\b/i.test(text)) return 'cloud';
680
+ if (insideWorkspaceCwd(options.cwd)) return 'local';
627
681
  return 'cloud';
628
682
  }
629
683
 
@@ -1165,6 +1219,51 @@ function formatApprovalTime(value) {
1165
1219
  return `${months[Number(month) - 1] || month} ${Number(day)}, ${year} at ${hour12}:${minute} ${ampm}`;
1166
1220
  }
1167
1221
 
1222
+ function previewText(value, limit = 80) {
1223
+ return String(value || '').replace(/\s+/g, ' ').trim().slice(0, limit);
1224
+ }
1225
+
1226
+ function previewContact(value) {
1227
+ if (!value) return '';
1228
+ if (typeof value === 'string') return previewText(value, 120);
1229
+ if (typeof value !== 'object') return previewText(value, 120);
1230
+ return previewText(value.email || value.address || value.value || value.name || '', 120);
1231
+ }
1232
+
1233
+ function previewContacts(value) {
1234
+ const values = Array.isArray(value)
1235
+ ? value
1236
+ : String(value || '').includes(',')
1237
+ ? String(value).split(',')
1238
+ : [value];
1239
+ return values
1240
+ .map(previewContact)
1241
+ .filter(Boolean)
1242
+ .slice(0, 3)
1243
+ .join(', ');
1244
+ }
1245
+
1246
+ function gmailApprovalDetails(receipt, request, approvalEvent = null) {
1247
+ const payload = request && request.payload && typeof request.payload === 'object' ? request.payload : {};
1248
+ const taskPreview = receipt && receipt.task_preview && typeof receipt.task_preview === 'object' ? receipt.task_preview : {};
1249
+ const slots = taskPreview.slots && typeof taskPreview.slots === 'object' ? taskPreview.slots : {};
1250
+ const preview = approvalEvent && approvalEvent.preview && typeof approvalEvent.preview === 'object' ? approvalEvent.preview : {};
1251
+ return {
1252
+ to: previewContacts(payload.to || payload.recipient || payload.recipients || payload.email || slots.to || slots.recipient || preview.to),
1253
+ subject: previewText(payload.subject || payload.title || slots.subject || preview.subject, 80),
1254
+ };
1255
+ }
1256
+
1257
+ function isGmailApproval(request, approvalEvent = null, accept = {}) {
1258
+ const connector = canonicalConnectorId((request && request.connector) || (approvalEvent && approvalEvent.tool));
1259
+ const action = String(
1260
+ (request && (request.executor_action_type || request.action_type || request.action))
1261
+ || accept.task
1262
+ || ''
1263
+ ).toLowerCase().replace(/_/g, ' ');
1264
+ return connector === 'gmail' && /\b(send|reply|compose)\b/.test(action);
1265
+ }
1266
+
1168
1267
  function formatApprovalReceipt(receipt) {
1169
1268
  if (!receipt || typeof receipt !== 'object') return null;
1170
1269
  const events = Array.isArray(receipt.tool_events) ? receipt.tool_events : [];
@@ -1182,6 +1281,15 @@ function formatApprovalReceipt(receipt) {
1182
1281
  return `Approval needed before creation: calendar event "${String(title).slice(0, 80)}"${when ? ` ${when}` : ''}`;
1183
1282
  }
1184
1283
 
1284
+ if (isGmailApproval(request, approvalEvent, accept)) {
1285
+ const details = gmailApprovalDetails(receipt, request, approvalEvent);
1286
+ const parts = [
1287
+ details.to ? `to ${details.to}` : '',
1288
+ details.subject ? `subject "${details.subject}"` : '',
1289
+ ].filter(Boolean);
1290
+ return `Approval needed before sending Gmail${parts.length ? `: ${parts.join(' ')}` : ''} (body hidden until approved)`;
1291
+ }
1292
+
1185
1293
  const connector = String(request.connector || approvalEvent.tool || 'connector').replace(/_/g, ' ');
1186
1294
  const action = String(request.action || actionType || 'action').replace(/_/g, ' ');
1187
1295
  return `Approval needed before ${action}: ${connector}`;
@@ -1210,6 +1318,16 @@ function formatTaskPreviewPlan(receipt) {
1210
1318
  if (!taskPreview) return null;
1211
1319
  const task = String(taskPreview.task || '').trim();
1212
1320
  const slots = taskPreview.slots && typeof taskPreview.slots === 'object' ? taskPreview.slots : {};
1321
+ const approvalEvent = taskPreviewApprovalEvent(receipt);
1322
+ const request = approvalEvent && approvalEvent.approval_request;
1323
+ if (isGmailApproval(request, approvalEvent, receipt.task_accept_receipt || {}) || task === 'gmail.send_message') {
1324
+ const details = gmailApprovalDetails(receipt, request || { payload: slots }, approvalEvent);
1325
+ return [
1326
+ 'send Gmail',
1327
+ details.to ? `to ${details.to}` : '',
1328
+ details.subject ? `subject "${details.subject}"` : '',
1329
+ ].filter(Boolean).join(' ');
1330
+ }
1213
1331
  if (task === 'calendar.create_event') {
1214
1332
  const title = taskPreviewTitle(receipt);
1215
1333
  const start = slots.start || (receipt.task_accept_receipt && receipt.task_accept_receipt.start);
@@ -1712,12 +1830,30 @@ function buildMessage(message, history = []) {
1712
1830
  return String(message || '').trim();
1713
1831
  }
1714
1832
 
1833
+ function createTurnId() {
1834
+ return `axt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}`;
1835
+ }
1836
+
1837
+ function connectorTurnPolicy(message, options = {}) {
1838
+ if (!connectorWriteIntent(message)) return null;
1839
+ const policy = {
1840
+ schema: 'ax.connector_turn.v1',
1841
+ scope: 'current_turn_only',
1842
+ policy: 'preview_then_explicit_approval',
1843
+ writes_require_approval: true,
1844
+ };
1845
+ if (options.turnId) policy.turn_id = String(options.turnId);
1846
+ if (options.conversationId) policy.conversation_id = String(options.conversationId);
1847
+ return policy;
1848
+ }
1849
+
1715
1850
  function buildPayload(message, options = {}) {
1716
1851
  const mode = normalizeMode(options.mode);
1717
1852
  const route = options.business ? 'local' : resolveRoute(message, options);
1718
1853
  const local = route !== 'cloud';
1719
1854
  const verifyCommand = String(options.verify || '').trim();
1720
1855
  const previousMessages = structuredHistory(options.history || []);
1856
+ const turnId = String(options.turnId || '').trim();
1721
1857
  const payload = {
1722
1858
  message: buildMessage(message, options.history || []),
1723
1859
  model: modelForMode(mode),
@@ -1730,6 +1866,14 @@ function buildPayload(message, options = {}) {
1730
1866
  if (options.conversationId) {
1731
1867
  payload.conversation_id = String(options.conversationId);
1732
1868
  }
1869
+ if (turnId) {
1870
+ payload.turn_id = turnId;
1871
+ }
1872
+ const connectorPolicy = !local ? connectorTurnPolicy(message, { ...options, turnId }) : null;
1873
+ if (connectorPolicy) {
1874
+ payload.connector_turn = connectorPolicy;
1875
+ payload.external_action_policy = 'preview_then_explicit_approval';
1876
+ }
1733
1877
  if (options.business) {
1734
1878
  // Business cloud workspace: the model loop stays on the backend and every
1735
1879
  // file/bash tool call relays here, executing on the business EC2 via the
@@ -1781,6 +1925,19 @@ function codeFastWorkspaceNotice(result) {
1781
1925
  function handleEvent(event, state, output) {
1782
1926
  if (!event || typeof event !== 'object') return;
1783
1927
  state.events.push(event);
1928
+ // AX_TIMING=1: phase scoreboard to stderr: one line per SSE event with ms
1929
+ // since turn start and since the previous event, so latency work can see
1930
+ // WHERE a slow turn spends its time (model TTFT vs tool exec vs loop count).
1931
+ if (process.env.AX_TIMING === '1') {
1932
+ const now = Date.now();
1933
+ if (!state._t0) state._t0 = now;
1934
+ const kind = event.type || event.status || 'event';
1935
+ const detail = event.type === 'assistant_blocks' && Array.isArray(event.blocks)
1936
+ ? event.blocks.map((b) => (b && b.type === 'tool_use' ? `tool:${b.tool}` : b && b.type)).join(',')
1937
+ : event.type === 'tool_results' ? `results:${(event.results || []).length}` : '';
1938
+ process.stderr.write(`[ax-timing] +${String(now - state._t0).padStart(6)}ms (gap ${String(now - (state._tPrev || now)).padStart(5)}ms) ${kind}${detail ? ' ' + detail : ''}\n`);
1939
+ state._tPrev = now;
1940
+ }
1784
1941
 
1785
1942
  if (event.type === 'system_init') {
1786
1943
  state.runtime = event;
@@ -1878,10 +2035,15 @@ function handleEvent(event, state, output) {
1878
2035
  }
1879
2036
 
1880
2037
  async function postTurn(message, options = {}) {
1881
- const route = options.business ? 'local' : resolveRoute(message, options);
2038
+ let route = options.business ? 'local' : resolveRoute(message, options);
2039
+ if (route === 'local' && !options.business && options.route !== 'local' && !options.forceLocal
2040
+ && !(await localBackendReachable())) {
2041
+ route = 'cloud';
2042
+ }
1882
2043
  const local = route !== 'cloud';
1883
2044
  const endpointRoute = options.business ? 'cloud' : route;
1884
2045
  const token = authToken();
2046
+ const turnId = options.turnId || createTurnId();
1885
2047
  const shouldSendConnectionContext = options.connectionContext
1886
2048
  || route === 'cloud'
1887
2049
  || mentionsConnector(message)
@@ -1890,13 +2052,17 @@ async function postTurn(message, options = {}) {
1890
2052
  ? await buildConnectionContext({ token, localWorkspace: local })
1891
2053
  : null);
1892
2054
  const connectionUserId = !local && isLoopbackBackend({ route: endpointRoute }) ? authUserId() : '';
1893
- const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
2055
+ const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId, turnId });
1894
2056
  const postData = JSON.stringify(payload);
1895
2057
  const output = options.output || process.stdout;
1896
2058
  // Relayed business turns wait on EC2 terminal calls (up to 60s each) with no
1897
2059
  // SSE traffic in between, so the socket-idle timeout needs more headroom.
1898
2060
  const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1899
- const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
2061
+ let timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
2062
+ // Local workspace tool loops (max_turns 8/14) legitimately run past the fast
2063
+ // lane's 60s chat wall — SwapBench 2026-07-02: three tool-loop tasks died at
2064
+ // ~60s as "Atris cloud did not respond". Same headroom as business relays.
2065
+ if (local) timeoutMs = Math.max(timeoutMs, 180000);
1900
2066
  const turnUrl = new URL(backendUrl({ route: endpointRoute }));
1901
2067
  const transport = turnUrl.protocol === 'https:' ? https : http;
1902
2068
  const state = {
@@ -2266,10 +2432,19 @@ async function chat(options = {}) {
2266
2432
  const pendingTaskPreview = latestPendingTaskPreview(history);
2267
2433
  const route = pendingTaskPreview ? 'cloud' : options.route;
2268
2434
  maybeWriteAtrisGoalBanner(output, cwd);
2269
- const turnRoute = options.business ? 'cloud' : resolveRoute(trimmed, { route });
2435
+ let turnRoute = options.business ? 'cloud' : resolveRoute(trimmed, { route, cwd });
2436
+ // Same downgrade as postTurn: an auto-routed local turn with no local
2437
+ // backend listening becomes a cloud turn instead of a blocked preflight
2438
+ // that dumps localhost start instructions at a consumer. Explicit --local
2439
+ // keeps the real error.
2440
+ if (turnRoute === 'local' && route !== 'local' && !options.forceLocal) {
2441
+ const probeLocal = options.localBackendProbe || localBackendReachable;
2442
+ if (!(await probeLocal())) turnRoute = 'cloud';
2443
+ }
2270
2444
  if (!(await ensureRuntimeReady(turnRoute))) return false;
2271
2445
  const turnFunction = options.turnFunction || turnFunctionForMode(mode);
2272
- const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId });
2446
+ const turnId = options.turnId || createTurnId();
2447
+ const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId, turnId });
2273
2448
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
2274
2449
  const approvalExecution = normalizeMode(mode) === 'code-fast'
2275
2450
  ? null
@@ -2331,7 +2506,7 @@ function printBackendHint(options = {}) {
2331
2506
  }
2332
2507
 
2333
2508
  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}`;
2509
+ return `cd ${process.env.HOME}/arena/atrisos-backend/backend && ATRIS2_ALLOW_LOCAL_WORKSPACE=1 ENVIRONMENT=development ENV=development ../venv/bin/uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`;
2335
2510
  }
2336
2511
 
2337
2512
  function formatBackendHint(options = {}) {
@@ -3219,7 +3394,14 @@ async function main() {
3219
3394
  console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
3220
3395
  } catch (error) {
3221
3396
  console.error(`x ${error.message}`);
3222
- printBackendHint({ route: route === 'auto' ? undefined : route });
3397
+ // Only name a lane when the user picked one. An auto-routed turn that
3398
+ // failed locally must not claim "Atris cloud did not respond" (seen live
3399
+ // 2026-07-02 after a local workspace-root rejection).
3400
+ if (route === 'auto') {
3401
+ console.error('\nRetry, or run ax --doctor to check the runtime.');
3402
+ } else {
3403
+ printBackendHint({ route });
3404
+ }
3223
3405
  process.exit(1);
3224
3406
  }
3225
3407
  }