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.
- package/AGENTS.md +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +6 -4
- package/atris/CLAUDE.md +8 -0
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +15 -0
- package/ax +189 -7
- package/bin/atris.js +273 -225
- package/commands/autoland.js +379 -0
- package/commands/autopilot-front.js +273 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +22 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +551 -19
- package/commands/mission.js +3674 -278
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run-front.js +144 -0
- package/commands/run.js +10 -7
- package/commands/sign.js +90 -0
- package/commands/strings.js +301 -0
- package/commands/task.js +782 -108
- package/commands/truth.js +171 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +391 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +78 -4
- package/lib/runner-command.js +51 -20
- package/lib/runs-prune.js +242 -0
- package/lib/task-db.js +19 -2
- package/lib/task-proof.js +1 -1
- package/package.json +4 -4
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 /
|
|
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
|
-
|
|
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
|
}
|