atris 3.30.6 → 3.30.8

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 CHANGED
@@ -61,6 +61,14 @@ Always-on agents should move proof-backed work to Review, complete their native
61
61
  goal, then continue the mission loop with the next goal. They must not run
62
62
  `atris task accept` or claim AgentXP unless a human approved the proof.
63
63
 
64
+ Mission-shaped user intent wins before normal task selection. If the user
65
+ message starts with `atris mission run`, execute that command first, then run
66
+ `atris mission goal --json` and mirror `goal.visible_goal` into the native chat
67
+ goal. If the user asks for a mission, loop, overnight run, "keep going", "fix
68
+ this", or sends a screenshot with task intent and no mission exists yet, start
69
+ one with `atris mission run "<inferred objective>" --owner <member>` instead of
70
+ falling back to manual task/worktree flow.
71
+
64
72
  Codex agents with native goal tools should run `atris mission goal --json`
65
73
  before choosing work. If the payload includes `goal.visible_goal`, mirror
66
74
  `goal.objective` into the visible chat goal when the current goal is empty,
@@ -77,7 +85,10 @@ CHECK → atris review (verify + cleanup)
77
85
 
78
86
  ## Parallel Member Worktrees
79
87
 
80
- When multiple members, Codex/Claude subagents, or other agents may touch a repo, start in an isolated checkout:
88
+ Default to the current checkout for small, clean, single-agent fixes. Use an
89
+ isolated checkout only when the launcher is dirty, multiple agents may edit in
90
+ parallel, proof will run for a long time, the change is risky, or release/publish
91
+ work needs a clean tree.
81
92
 
82
93
  ```bash
83
94
  atris worktree guide
@@ -87,7 +98,7 @@ cd <printed path>
87
98
  atris worktree ship --message "<commit summary>" --verify "<test command>" --merge
88
99
  ```
89
100
 
90
- This ties member/agent identity, mission/member state, branch name, isolated checkout, optional Swarlo claim, verification, push, PR, and merge together. Use `atris worktree status` before broad staging or cleanup.
101
+ This ties member/agent identity, mission/member state, branch name, isolated checkout, optional Swarlo claim, verification, push, PR, and merge together. Use `atris worktree status` before broad staging and `atris worktree cleanup` / `atris worktree cleanup --apply` to remove clean merged worktrees.
91
102
 
92
103
  ## Mission Autonomy
93
104
 
package/README.md CHANGED
@@ -91,7 +91,7 @@ Integrates with any agent.
91
91
 
92
92
  ```bash
93
93
  cd your-project
94
- ax chat ax --fast
94
+ ax --fast --chat
95
95
  ax --fast "hello"
96
96
  ax --pro "help me plan this task"
97
97
  ax --max "reason through this hard decision"
package/atris/AGENTS.md CHANGED
@@ -22,6 +22,9 @@ section.
22
22
  ## Mission Autonomy
23
23
 
24
24
  Use `atris mission` when work should survive this chat or run as an autonomous loop.
25
+ Mission-shaped user intent wins before normal task selection: if the user says
26
+ `atris mission run ...`, execute it first, then run `atris mission goal --json`
27
+ and mirror `goal.visible_goal`.
25
28
 
26
29
  ```
27
30
  member -> mission start --verify -> status --status active -> one bounded step -> mission tick --verify -> receipt -> complete|run|stop
@@ -62,6 +65,10 @@ before choosing work. If the payload includes `goal.visible_goal`, mirror
62
65
  complete, or already matching; never complete unrelated active goals just to
63
66
  make room.
64
67
 
68
+ Default to the current checkout for small, clean, single-agent fixes. Use
69
+ worktrees only for dirty launchers, parallel agents, long proof, risky edits, or
70
+ release/publish work; clean old merged worktrees with `atris worktree cleanup`.
71
+
65
72
  Do not write new operating doctrine here first. Add it to Atris policy, skills,
66
73
  wiki, or `atris/atris.md`, then regenerate this adapter if needed.
67
74
 
package/atris/CLAUDE.md CHANGED
@@ -70,6 +70,9 @@ Do not write new operating doctrine here first. Add it to Atris policy, skills,
70
70
  ## Mission Autonomy
71
71
 
72
72
  Use `atris mission` when work should survive this chat or run as an autonomous loop.
73
+ Mission-shaped user intent wins before normal task selection: if the user says
74
+ `atris mission run ...`, execute it first, then run `atris mission goal --json`
75
+ and mirror `goal.visible_goal`.
73
76
 
74
77
  ```
75
78
  member -> mission start --verify -> status --status active -> one bounded step -> mission tick --verify -> receipt -> complete|run|stop
@@ -81,6 +84,10 @@ member -> mission start --verify -> status --status active -> one bounded step -
81
84
  - Prove: after one bounded step, run `atris mission tick <id> --verify --summary "<what changed>"`.
82
85
  - Close: if the verifier passes, run `atris mission complete <id> --proof "<receipt_path>"`; if current-agent work should keep going, repeat status -> step -> tick.
83
86
 
87
+ Default to the current checkout for small, clean, single-agent fixes. Use
88
+ worktrees only for dirty launchers, parallel agents, long proof, risky edits, or
89
+ release/publish work; clean old merged worktrees with `atris worktree cleanup`.
90
+
84
91
  If the task produces durable project knowledge, update `atris/wiki/` or run the local wiki flow (`atris ingest`, `atris query`, `atris lint`).
85
92
 
86
93
  ## Rules (Non‑Negotiable)
package/atris/GEMINI.md CHANGED
@@ -39,6 +39,9 @@ This is the Atris boot sequence. Show the output to the user, then respond natur
39
39
  ## Mission Autonomy
40
40
 
41
41
  Use `atris mission` when work should survive this chat or run as an autonomous loop.
42
+ Mission-shaped user intent wins before normal task selection: if the user says
43
+ `atris mission run ...`, execute it first, then run `atris mission goal --json`
44
+ and mirror `goal.visible_goal`.
42
45
 
43
46
  ```
44
47
  member -> mission start --verify -> status --status active -> one bounded step -> mission tick --verify -> receipt -> complete|run|stop
@@ -50,6 +53,10 @@ member -> mission start --verify -> status --status active -> one bounded step -
50
53
  - Prove: after one bounded step, run `atris mission tick <id> --verify --summary "<what changed>"`.
51
54
  - Close: if the verifier passes, run `atris mission complete <id> --proof "<receipt_path>"`; if current-agent work should keep going, repeat status -> step -> tick.
52
55
 
56
+ Default to the current checkout for small, clean, single-agent fixes. Use
57
+ worktrees only for dirty launchers, parallel agents, long proof, risky edits, or
58
+ release/publish work; clean old merged worktrees with `atris worktree cleanup`.
59
+
53
60
  ## Rules
54
61
 
55
62
  - Plan = ASCII visualization + approval gate. Do not execute during planning.
@@ -28,6 +28,7 @@ Detect: Project has `atris/` folder with MAP.md, TODO.md, PERSONA.md
28
28
  Commands: plan → do → review
29
29
 
30
30
  Key behaviors:
31
+ - Mission-shaped intent wins: run `atris mission run ...` before normal task selection when the user invokes it or asks for a loop.
31
32
  - Read PERSONA.md (3-4 sentences, ASCII visuals)
32
33
  - Check MAP.md for file:line refs
33
34
  - Use `atris task` for claims, proof, ready, and accept
@@ -35,6 +36,24 @@ Key behaviors:
35
36
  - Treat TODO.md as a rendered view; regenerate it instead of hand-editing tasks
36
37
  - In Codex runtimes with native goal tools, mirror `atris mission goal --json` into the visible chat goal via `goal.visible_goal`
37
38
 
39
+ ## Mission Intent Intercept
40
+
41
+ Before checking existing tasks, inspect the user's message. If it starts with
42
+ `atris mission run`, execute that command first. Then run
43
+ `atris mission goal --json` and mirror `goal.visible_goal` into the runtime's
44
+ native goal when available.
45
+
46
+ If the user asks for a mission, loop, overnight run, "keep going", "fix this",
47
+ or sends a screenshot with task intent and no mission exists yet, start one with
48
+ `atris mission run "<inferred objective>" --owner <member>` instead of falling
49
+ back to manual task/worktree flow.
50
+
51
+ Worktrees are not the default. Stay in the current checkout for small, clean,
52
+ single-agent fixes. Use `atris worktree start` only for dirty launchers,
53
+ parallel agents, long proof runs, risky edits, or release/publish work. Use
54
+ `atris worktree cleanup` to preview cleanup and `atris worktree cleanup --apply`
55
+ to remove clean merged worktrees.
56
+
38
57
  ## Visible Chat Goal Mirror
39
58
 
40
59
  Before choosing work in an Atris repo, run:
@@ -59,7 +78,8 @@ goal after proof is ready; only a human should run `atris task accept`.
59
78
 
60
79
  1. Run `atris atris.md` on first interaction to show workspace status
61
80
  2. Read `atris/MAP.md` before any file search to find file:line refs
62
- 3. Run `atris mission goal --json` and mirror `goal.visible_goal` into the native chat goal when the runtime supports it
63
- 4. Run `atris task list` or `atris task next` to find current work
64
- 5. Claim tasks with `atris task claim <id> --as <functional-member>`
65
- 6. Move completed work to review with `atris task ready <id> --proof "..."`
81
+ 3. If user intent is mission-shaped, run `atris mission run ...` before task flow
82
+ 4. Run `atris mission goal --json` and mirror `goal.visible_goal` into the native chat goal when the runtime supports it
83
+ 5. Run `atris task list` or `atris task next` only after mission intent is handled
84
+ 6. Claim tasks with `atris task claim <id> --as <functional-member>`
85
+ 7. Move completed work to review with `atris task ready <id> --proof "..."`
package/ax CHANGED
@@ -17,6 +17,7 @@ const BACKEND = {
17
17
  path: '/api/atris2/turn'
18
18
  };
19
19
  const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
20
+ const CLOUD_BACKEND_BASE = 'https://api.atris.ai';
20
21
  const CODE_FAST = {
21
22
  path: '/api/cursor/turn',
22
23
  model: 'composer-2-5-fast',
@@ -124,11 +125,11 @@ function formatUsage() {
124
125
  ' ax [--max|--fast] --benchmark',
125
126
  '',
126
127
  'Modes:',
127
- ' --max local workspace agent, highest reasoning, slowest turns',
128
- ' --pro local workspace agent, deeper tool loop',
129
- ' --fast local workspace agent, faster low-latency turns',
128
+ ' --max highest reasoning, slowest turns',
129
+ ' --pro deeper tool loop',
130
+ ' --fast faster low-latency turns',
130
131
  ' --code-fast Atris Code Fast public lane',
131
- ' --local force local workspace tools',
132
+ ' --local force local workspace tools with your own AtrisOS backend',
132
133
  ' --cloud force authenticated cloud connectors/chat',
133
134
  ' --business <slug> run tools on that business cloud workspace (EC2)',
134
135
  ' --verify <cmd> gate the turn on this command passing (default: no verifier)',
@@ -209,19 +210,20 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
209
210
  };
210
211
  }
211
212
 
212
- function backendBaseUrl() {
213
- return (process.env.AX_BACKEND_URL
213
+ function backendBaseUrl(options = {}) {
214
+ const explicitBase = process.env.AX_BACKEND_URL
214
215
  || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
215
- || process.env.OBELISK_ATRIS2_BACKEND_URL
216
- || DEFAULT_BACKEND_BASE).replace(/\/$/, '');
216
+ || process.env.OBELISK_ATRIS2_BACKEND_URL;
217
+ if (explicitBase) return explicitBase.replace(/\/$/, '');
218
+ return options.route === 'cloud' ? CLOUD_BACKEND_BASE : DEFAULT_BACKEND_BASE;
217
219
  }
218
220
 
219
- function backendUrl() {
220
- return new URL(BACKEND.path, backendBaseUrl()).toString();
221
+ function backendUrl(options = {}) {
222
+ return new URL(BACKEND.path, backendBaseUrl(options)).toString();
221
223
  }
222
224
 
223
- function backendPathUrl(pathname) {
224
- return new URL(pathname, backendBaseUrl()).toString();
225
+ function backendPathUrl(pathname, options = {}) {
226
+ return new URL(pathname, backendBaseUrl(options)).toString();
225
227
  }
226
228
 
227
229
  function codeFastBaseUrl() {
@@ -266,10 +268,11 @@ function buildRunProfile(options = {}) {
266
268
  reasoning: 'Composer 2.5 fast lane; charges 10 credits per public turn'
267
269
  };
268
270
  }
269
- const route = resolveRoute(options.message || 'doctor', options);
270
- const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
271
+ const profileMessage = options.message || 'what files are here?';
272
+ const route = resolveRoute(profileMessage, options);
273
+ const payload = buildPayload(profileMessage, { mode, cwd, route });
271
274
  return {
272
- endpoint: backendUrl(),
275
+ endpoint: backendUrl({ route }),
273
276
  mode,
274
277
  route,
275
278
  model: payload.model,
@@ -302,7 +305,7 @@ function formatRunProfile(profile, options = {}) {
302
305
  async function buildRuntimeHealth(options = {}) {
303
306
  const healthRes = options.healthRes
304
307
  ? await Promise.resolve(options.healthRes)
305
- : await requestJson(ATRIS2_HEALTH_PATH, { token: '', timeoutMs: options.timeoutMs || 1500 });
308
+ : await requestJson(ATRIS2_HEALTH_PATH, { token: '', timeoutMs: options.timeoutMs || 1500, route: options.route || 'local' });
306
309
  const data = healthRes.ok && healthRes.data && typeof healthRes.data === 'object' ? healthRes.data : {};
307
310
  const models = Array.isArray(data.models) ? data.models : [];
308
311
  const fast = models.find(row => row && row.id === 'atris:fast') || null;
@@ -335,7 +338,7 @@ function formatRuntimeHealth(health, options = {}) {
335
338
  ['fast', fastReady ? 'ready' : 'not ready'],
336
339
  ['approvals', permissionReady ? 'ready' : 'offline'],
337
340
  ];
338
- if (!backendReachable) rows.push(['fix', 'start local backend, then rerun ax --doctor']);
341
+ if (!backendReachable) rows.push(['fix', 'local backend offline; use hosted mode or start your AtrisOS backend']);
339
342
  return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
340
343
  }
341
344
 
@@ -378,17 +381,17 @@ function authUserId() {
378
381
  }
379
382
  }
380
383
 
381
- function isLoopbackBackend() {
384
+ function isLoopbackBackend(options = {}) {
382
385
  try {
383
- const parsed = new URL(backendBaseUrl());
386
+ const parsed = new URL(backendBaseUrl(options));
384
387
  return ['127.0.0.1', 'localhost', '::1'].includes(parsed.hostname);
385
388
  } catch (_) {
386
389
  return false;
387
390
  }
388
391
  }
389
392
 
390
- function requestJson(pathname, { token = authToken(), timeoutMs = 6000 } = {}) {
391
- const url = backendPathUrl(pathname);
393
+ function requestJson(pathname, { token = authToken(), timeoutMs = 6000, route } = {}) {
394
+ const url = backendPathUrl(pathname, { route });
392
395
  const parsed = new URL(url);
393
396
  const transport = parsed.protocol === 'https:' ? https : http;
394
397
  return new Promise((resolve) => {
@@ -422,8 +425,8 @@ function requestJson(pathname, { token = authToken(), timeoutMs = 6000 } = {}) {
422
425
  });
423
426
  }
424
427
 
425
- function postJson(pathname, body, { token = authToken(), timeoutMs = 30000 } = {}) {
426
- const url = backendPathUrl(pathname);
428
+ function postJson(pathname, body, { token = authToken(), timeoutMs = 30000, route } = {}) {
429
+ const url = backendPathUrl(pathname, { route });
427
430
  const parsed = new URL(url);
428
431
  const transport = parsed.protocol === 'https:' ? https : http;
429
432
  const postData = JSON.stringify(body || {});
@@ -524,7 +527,7 @@ function cachedIntegrationStatus(options = {}) {
524
527
 
525
528
  async function buildConnectionContext(options = {}) {
526
529
  const token = options.token || authToken();
527
- const localStatusUserId = isLoopbackBackend() ? authUserId() : '';
530
+ const localStatusUserId = isLoopbackBackend({ route: options.route }) ? authUserId() : '';
528
531
  const statusPath = localStatusUserId
529
532
  ? `${ATRIS2_CONNECTION_STATUS_PATH}?connection_user_id=${encodeURIComponent(localStatusUserId)}`
530
533
  : CONNECTION_STATUS_PATH;
@@ -532,9 +535,9 @@ async function buildConnectionContext(options = {}) {
532
535
  options.statusRes
533
536
  ? Promise.resolve(options.statusRes)
534
537
  : localStatusUserId || token
535
- ? requestJson(statusPath, { token: localStatusUserId ? '' : token })
538
+ ? requestJson(statusPath, { token: localStatusUserId ? '' : token, route: options.route })
536
539
  : Promise.resolve({ ok: false, data: null }),
537
- options.contractRes ? Promise.resolve(options.contractRes) : requestJson(CONNECTION_CAPABILITIES_PATH, { token: '' })
540
+ options.contractRes ? Promise.resolve(options.contractRes) : requestJson(CONNECTION_CAPABILITIES_PATH, { token: '', route: options.route })
538
541
  ]);
539
542
  const statusData = statusRes.ok && statusRes.data && typeof statusRes.data === 'object' ? statusRes.data : {};
540
543
  const backendStatuses = statusData.statuses && typeof statusData.statuses === 'object' ? statusData.statuses : statusData;
@@ -595,7 +598,15 @@ function connectorWriteIntent(message) {
595
598
  }
596
599
 
597
600
  function workspaceIntent(message) {
598
- return /\b(files?|folders?|repo|workspace|project|directory|tree|read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|tests?|build|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map)\b/i.test(message || '');
601
+ return Boolean(workspaceIntentReason(message));
602
+ }
603
+
604
+ function workspaceIntentReason(message) {
605
+ const text = String(message || '');
606
+ if (/\b(files?|folders?|repo|workspace|project|directory|tree|src|source|code|diff|git|backend|frontend|atris task|atris xp|xp game|career xp|agentxp|todo|map|tests?)\b/i.test(text)) return 'prompt mentions workspace, file, code, or repo terms';
607
+ if (/(^|\s|["'`])(?:\.{0,2}\/)?[\w.-]+\/[\w./-]+|[\w.-]+\.(?:js|jsx|ts|tsx|mjs|cjs|json|md|py|rb|go|rs|java|css|scss|html|yml|yaml|toml|env|txt)\b/i.test(text)) return 'prompt names a file path or filename';
608
+ if (/\b(read|open|inspect|search|grep|find|locate|where|edit|write|change|modify|patch|fix|test|build|refactor)\b.*\b(this|file|folder|repo|workspace|project|directory|module|component|package|readme|source|code|branch|suite|build|bug)\b/i.test(text)) return 'prompt asks to operate on workspace material';
609
+ return '';
599
610
  }
600
611
 
601
612
  function githubWorkspaceIntent(message) {
@@ -605,11 +616,22 @@ function githubWorkspaceIntent(message) {
605
616
  }
606
617
 
607
618
  function resolveRoute(message, options = {}) {
608
- if (options.route === 'local' || options.forceLocal) return 'local';
609
- if (options.route === 'cloud' || options.forceCloud) return 'cloud';
610
- if (githubWorkspaceIntent(message)) return 'local';
611
- if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
612
- return 'local';
619
+ return routeDecision(message, options).route;
620
+ }
621
+
622
+ function routeDecision(message, options = {}) {
623
+ if (options.route === 'local' || options.forceLocal) return { route: 'local', reason: 'explicit --local' };
624
+ if (options.route === 'cloud' || options.forceCloud) return { route: 'cloud', reason: 'explicit --cloud' };
625
+ if (githubWorkspaceIntent(message)) return { route: 'local', reason: 'prompt asks for GitHub repo mutation' };
626
+ const reason = workspaceIntentReason(message);
627
+ if (mentionsConnector(message) && !reason) return { route: 'cloud', reason: 'connector request uses hosted Atris' };
628
+ if (reason) return { route: 'local', reason };
629
+ return { route: 'cloud', reason: 'default hosted chat' };
630
+ }
631
+
632
+ function shouldPreflightRuntime(route, options = {}, mode = 'fast') {
633
+ if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') return false;
634
+ return route !== 'cloud';
613
635
  }
614
636
 
615
637
  function normalizeMode(mode) {
@@ -1323,11 +1345,11 @@ function creditsFromApprovalExecution(response) {
1323
1345
  return Number.isFinite(credits) && credits > 0 ? credits : null;
1324
1346
  }
1325
1347
 
1326
- async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken() } = {}) {
1348
+ async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken(), route = 'cloud' } = {}) {
1327
1349
  return postJson(APPROVAL_EXECUTE_PATH, {
1328
1350
  model,
1329
1351
  approval_request: approvalRequest,
1330
- }, { token });
1352
+ }, { token, route });
1331
1353
  }
1332
1354
 
1333
1355
  async function executeApprovalRequest(approvalRequest, {
@@ -1716,9 +1738,9 @@ async function postTurn(message, options = {}) {
1716
1738
  || mentionsConnector(message)
1717
1739
  || /\b(can you use|can you access|connected|connections?|integrations?|tools?|capabilities)\b/i.test(message || '');
1718
1740
  const connectionContext = options.connectionContext || (shouldSendConnectionContext
1719
- ? await buildConnectionContext({ token, localWorkspace: local })
1741
+ ? await buildConnectionContext({ token, localWorkspace: local, route })
1720
1742
  : null);
1721
- const connectionUserId = !local && isLoopbackBackend() ? authUserId() : '';
1743
+ const connectionUserId = !local && isLoopbackBackend({ route }) ? authUserId() : '';
1722
1744
  const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
1723
1745
  const postData = JSON.stringify(payload);
1724
1746
  const output = options.output || process.stdout;
@@ -1726,7 +1748,7 @@ async function postTurn(message, options = {}) {
1726
1748
  // SSE traffic in between, so the socket-idle timeout needs more headroom.
1727
1749
  const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1728
1750
  const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
1729
- const turnUrl = new URL(backendUrl());
1751
+ const turnUrl = new URL(backendUrl({ route }));
1730
1752
  const transport = turnUrl.protocol === 'https:' ? https : http;
1731
1753
  const state = {
1732
1754
  events: [],
@@ -1968,23 +1990,22 @@ async function chat(options = {}) {
1968
1990
  output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
1969
1991
  if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
1970
1992
 
1971
- const runtimePreflight = { ready: false };
1972
- const ensureRuntimeReady = async () => {
1973
- if (runtimePreflight.ready) return true;
1974
- if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') {
1975
- runtimePreflight.ready = true;
1993
+ const runtimePreflight = { localReady: false };
1994
+ const ensureRuntimeReady = async (routeForTurn = 'local', decision = {}) => {
1995
+ if (!shouldPreflightRuntime(routeForTurn, options, mode)) {
1976
1996
  return true;
1977
1997
  }
1998
+ if (runtimePreflight.localReady) return true;
1978
1999
  const startedAt = Date.now();
1979
2000
  const health = options.runtimeHealth
1980
2001
  ? await Promise.resolve(options.runtimeHealth({ mode }))
1981
- : await buildRuntimeHealth({ timeoutMs: options.preflightTimeoutMs || 1200 });
2002
+ : await buildRuntimeHealth({ timeoutMs: options.preflightTimeoutMs || 1200, route: 'local' });
1982
2003
  if (runtimeReadyForChat(health, mode)) {
1983
- runtimePreflight.ready = true;
2004
+ runtimePreflight.localReady = true;
1984
2005
  return true;
1985
2006
  }
1986
2007
  output.write(`${formatRuntimeHealth(health, output)}\n\n`);
1987
- output.write(`${formatBackendHint()}\n`);
2008
+ output.write(`${formatBackendHint({ routeReason: decision.reason, explicitLocal: decision.reason === 'explicit --local' })}\n`);
1988
2009
  output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
1989
2010
  return false;
1990
2011
  };
@@ -2025,7 +2046,7 @@ async function chat(options = {}) {
2025
2046
  return false;
2026
2047
  }
2027
2048
  if (pendingApprovalRequest && normalizeMode(mode) !== 'code-fast' && messageApprovesPendingApproval(trimmed)) {
2028
- if (!(await ensureRuntimeReady())) return false;
2049
+ if (!(await ensureRuntimeReady('cloud'))) return false;
2029
2050
  const startedAt = Date.now();
2030
2051
  const approvalState = {
2031
2052
  events: [],
@@ -2070,8 +2091,11 @@ async function chat(options = {}) {
2070
2091
  }
2071
2092
 
2072
2093
  const pendingTaskPreview = latestPendingTaskPreview(history);
2073
- const route = pendingTaskPreview ? 'cloud' : options.route;
2074
- if (!(await ensureRuntimeReady())) return false;
2094
+ const decision = pendingTaskPreview
2095
+ ? { route: 'cloud', reason: 'pending approval stays on hosted Atris' }
2096
+ : routeDecision(trimmed, { route: options.route });
2097
+ const route = decision.route;
2098
+ if (!(await ensureRuntimeReady(route, decision))) return false;
2075
2099
  const turnFunction = options.turnFunction || turnFunctionForMode(mode);
2076
2100
  const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId });
2077
2101
  if (result.output && !result.output.endsWith('\n')) output.write('\n');
@@ -2129,17 +2153,21 @@ async function chat(options = {}) {
2129
2153
  if (logger) logger.close(0);
2130
2154
  }
2131
2155
 
2132
- function printBackendHint() {
2156
+ function printBackendHint(options = {}) {
2133
2157
  console.log('');
2134
- console.log(formatBackendHint());
2158
+ console.log(formatBackendHint(options));
2135
2159
  }
2136
2160
 
2137
- function backendStartCommand() {
2138
- 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}`;
2161
+ function backendStartCommand(options = {}) {
2162
+ if (options.explicitLocal) return 'Start your AtrisOS backend, or retry with --cloud for hosted chat.';
2163
+ return 'Start your AtrisOS backend for local workspace, or retry with --cloud for hosted cloud scratch.';
2139
2164
  }
2140
2165
 
2141
- function formatBackendHint() {
2142
- return `Start backend:\n${backendStartCommand()}`;
2166
+ function formatBackendHint(options = {}) {
2167
+ const lines = ['This requires local workspace mode, but no AtrisOS backend is running.'];
2168
+ if (options.routeReason && !options.explicitLocal) lines.push(`Routed local because ${options.routeReason}.`);
2169
+ lines.push(backendStartCommand(options));
2170
+ return lines.join('\n');
2143
2171
  }
2144
2172
 
2145
2173
  function bufferedOutput() {
@@ -2257,6 +2285,7 @@ async function runSelfTest(options = {}) {
2257
2285
  try {
2258
2286
  await chat({
2259
2287
  mode: 'fast',
2288
+ route: 'local',
2260
2289
  cwd: os.tmpdir(),
2261
2290
  input: Readable.from(['hi\n', 'exit\n']),
2262
2291
  output: chatOutput,
@@ -2277,7 +2306,9 @@ async function runSelfTest(options = {}) {
2277
2306
  const text = chunks.join('');
2278
2307
  assertSelfTest(healthCalls === 1, 'offline preflight repeated unexpectedly');
2279
2308
  assertSelfTest(/backend\s+offline/.test(text), 'offline backend status missing');
2280
- assertSelfTest(/Start backend:/.test(text) && /uvicorn main:app/.test(text), 'start-backend command missing');
2309
+ assertSelfTest(/This requires local workspace mode, but no AtrisOS backend is running\./.test(text), 'local backend hint missing');
2310
+ assertSelfTest(/retry with --cloud for hosted chat/.test(text), 'cloud retry hint missing');
2311
+ assertSelfTest(!/\/Users\/keshavrao/.test(text), 'local backend hint leaked a developer path');
2281
2312
  assertSelfTest(!/secret-token/.test(text), 'offline preflight leaked error detail');
2282
2313
  }, results, output);
2283
2314
 
@@ -3001,7 +3032,11 @@ async function main() {
3001
3032
  console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
3002
3033
  } catch (error) {
3003
3034
  console.error(`x ${error.message}`);
3004
- printBackendHint();
3035
+ const decision = prompt
3036
+ ? routeDecision(prompt, { route: route === 'auto' ? undefined : route })
3037
+ : { route: forceLocal ? 'local' : 'cloud', reason: forceLocal ? 'explicit --local' : 'default hosted chat' };
3038
+ const shouldShowBackendHint = decision.route === 'local';
3039
+ if (shouldShowBackendHint) printBackendHint({ routeReason: decision.reason, explicitLocal: forceLocal || decision.reason === 'explicit --local' });
3005
3040
  process.exit(1);
3006
3041
  }
3007
3042
  }
@@ -3071,6 +3106,7 @@ module.exports = {
3071
3106
  renderStreamingMarkdown,
3072
3107
  renderTerminalMarkdown,
3073
3108
  resolveRoute,
3109
+ shouldPreflightRuntime,
3074
3110
  runBenchmark,
3075
3111
  runSelfTest,
3076
3112
  runtimeReadyForChat,
package/commands/init.js CHANGED
@@ -671,6 +671,14 @@ Always-on agents should move proof-backed work to Review, complete their native
671
671
  goal, then continue the mission loop with the next goal. They must not run
672
672
  \`atris task accept\` or claim AgentXP unless a human approved the proof.
673
673
 
674
+ Mission-shaped user intent wins before normal task selection. If the user
675
+ message starts with \`atris mission run\`, execute that command first, then run
676
+ \`atris mission goal --json\` and mirror \`goal.visible_goal\` into the native chat
677
+ goal. If the user asks for a mission, loop, overnight run, "keep going", "fix
678
+ this", or sends a screenshot with task intent and no mission exists yet, start
679
+ one with \`atris mission run "<inferred objective>" --owner <member>\` instead of
680
+ falling back to manual task/worktree flow.
681
+
674
682
  Task owners are functional or feature members, not engines. Use \`task-planner\`,
675
683
  \`architect\`, \`mission-lead\`, \`validator\`, \`launcher\`, or a feature owner for
676
684
  assignment; put coding agent models like Codex and Claude in the \`executed_by\`
@@ -684,6 +692,15 @@ BUILD → atris do (execute tasks)
684
692
  CHECK → atris review (verify + cleanup)
685
693
  \`\`\`
686
694
 
695
+ ## Worktrees
696
+
697
+ Default to the current checkout for small, clean, single-agent fixes. Use
698
+ \`atris worktree start\` only when the launcher is dirty, multiple agents may
699
+ edit in parallel, proof will run for a long time, the change is risky, or
700
+ release/publish work needs a clean tree. Use \`atris worktree cleanup\` to preview
701
+ merged worktree removal and \`atris worktree cleanup --apply\` to remove clean
702
+ merged worktrees.
703
+
687
704
  ## Mission Autonomy
688
705
 
689
706
  Use \`atris mission\` when work should survive this chat or run as an autonomous loop.
@@ -1888,11 +1888,13 @@ function codexVisibleGoalBridge(mission, goalObjective) {
1888
1888
  keep_if_matching: 'if current goal objective equals goal.objective, continue the mission',
1889
1889
  create_when_empty_or_completed: 'create_goal({ objective: goal.objective })',
1890
1890
  complete_after_proof: 'update_goal({ status: "complete" })',
1891
+ refresh_on_phase_change: 'atris mission goal --json before continuing changed work',
1891
1892
  refresh_next_candidate: 'atris mission goal --json',
1892
1893
  },
1893
1894
  guardrails: [
1894
1895
  'Do not complete a human-set active goal unless it matches this mission goal or the mission receipt proves handoff.',
1895
1896
  'If create_goal fails because another goal is active, keep this bridge waiting for the visible goal slot.',
1897
+ 'When the work changes phase or objective, refresh the mission goal before continuing.',
1896
1898
  ],
1897
1899
  };
1898
1900
  }
@@ -1906,7 +1908,8 @@ function codexGoalToolContract(mission) {
1906
1908
  set_next_goal: 'use goal.visible_goal: create_goal({ objective: goal.objective }) when no active goal blocks the slot',
1907
1909
  visible_goal_bridge: 'goal.visible_goal',
1908
1910
  platform_requirement: 'Codex runtime must expose replace_goal/set_goal, or allow update_goal({ status: "complete" }) followed by create_goal({ objective }).',
1909
- runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof -> atris mission goal --json -> create_goal({ objective: goal.objective })',
1911
+ phase_change_refresh: 'before changed follow-up work, run atris mission goal --json and mirror the returned visible goal',
1912
+ runtime_tool_sequence: 'get_goal -> update_goal({ status: "complete" }) after proof or phase change -> atris mission goal --json -> create_goal({ objective: goal.objective })',
1910
1913
  blocked_without_platform_goal_write: true,
1911
1914
  mission_id: mission.id,
1912
1915
  };
@@ -2422,6 +2425,10 @@ function cappedClaudeReceiptText(text, limit = 4000) {
2422
2425
 
2423
2426
  async function runMission(args) {
2424
2427
  const asJson = wantsJson(args);
2428
+ if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
2429
+ help();
2430
+ return;
2431
+ }
2425
2432
  const dueMode = hasFlag(args, '--due');
2426
2433
  const skipClaude = hasFlag(args, '--no-claude');
2427
2434
  const verifyEach = !hasFlag(args, '--no-verify');
@@ -3277,6 +3284,7 @@ atris mission - durable goal + loop + owner + proof state
3277
3284
  atris mission goal [--heartbeat] [--json]
3278
3285
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
3279
3286
  atris mission tick <id> [--verify] [--complete-on-pass] [--summary "..."] [--json]
3287
+ atris mission "<objective>" [--owner <member>] Shortcut for: atris mission run "<objective>"
3280
3288
  atris mission run ["objective"|<member> ["objective"]|id|--due] [--owner <member>] [--max-ticks 4] [--max-wall 3600] [--cadence "15m"]
3281
3289
  [--no-claude] [--no-verify] [--complete-on-pass] [--no-drain] [--json]
3282
3290
  (bare/member-only run prompts; one-word fuzzy intent starts a new visible-goal mission; --due runs the saved queue)
@@ -3513,6 +3521,7 @@ function missionCommand(args) {
3513
3521
  case '-h':
3514
3522
  return help();
3515
3523
  default:
3524
+ if (subcommand && !String(subcommand).startsWith('-')) return runMission(args);
3516
3525
  return help();
3517
3526
  }
3518
3527
  }
@@ -59,7 +59,10 @@ function parseWorktrees(text) {
59
59
  if (current.worktree) {
60
60
  let branch = current.branch || 'detached';
61
61
  branch = branch.replace(/^refs\/heads\//, '');
62
- out.push({ path: current.worktree, branch, head: current.HEAD || '' });
62
+ const item = { path: current.worktree, branch, head: current.HEAD || '' };
63
+ if (current.locked) item.locked = true;
64
+ if (current.prunable) item.prunable = true;
65
+ out.push(item);
63
66
  }
64
67
  current = {};
65
68
  continue;
@@ -451,10 +454,98 @@ function prune(args) {
451
454
  return result.status || 0;
452
455
  }
453
456
 
457
+ const PROTECTED_BRANCHES = new Set(['main', 'master']);
458
+
459
+ function cleanupWorktrees({ root = repoRoot(), base: baseOverride = '', apply = false } = {}) {
460
+ const worktrees = listWorktrees(root);
461
+ const primary = worktrees[0]?.path ? path.resolve(worktrees[0].path) : '';
462
+ const current = path.resolve(root);
463
+ const base = normalizeTargetRef(root, baseOverride || defaultShipTarget(root));
464
+ refreshRemoteRef(root, base);
465
+ const candidates = [];
466
+ const kept = [];
467
+ const removed = [];
468
+
469
+ for (const wt of worktrees) {
470
+ const wtPath = path.resolve(wt.path);
471
+ const item = { path: wt.path, branch: wt.branch, head: wt.head };
472
+ if (wtPath === primary) {
473
+ kept.push({ ...item, reason: 'primary_checkout' });
474
+ continue;
475
+ }
476
+ if (wtPath === current) {
477
+ kept.push({ ...item, reason: 'current_checkout' });
478
+ continue;
479
+ }
480
+ if (wt.locked) {
481
+ kept.push({ ...item, reason: 'locked' });
482
+ continue;
483
+ }
484
+ if (PROTECTED_BRANCHES.has(wt.branch)) {
485
+ kept.push({ ...item, reason: 'protected_branch' });
486
+ continue;
487
+ }
488
+ const counts = statusCounts(wt.path);
489
+ if (!counts) {
490
+ kept.push({ ...item, reason: 'missing_or_unreadable' });
491
+ continue;
492
+ }
493
+ if (counts.staged || counts.unstaged || counts.untracked) {
494
+ kept.push({ ...item, reason: 'dirty', staged: counts.staged, unstaged: counts.unstaged, untracked: counts.untracked });
495
+ continue;
496
+ }
497
+ if (!wt.head) {
498
+ kept.push({ ...item, reason: 'missing_head' });
499
+ continue;
500
+ }
501
+ const merged = runGit(['merge-base', '--is-ancestor', wt.head, base], { cwd: root, check: false }).status === 0;
502
+ if (!merged) {
503
+ kept.push({ ...item, reason: 'unmerged' });
504
+ continue;
505
+ }
506
+ const candidate = { ...item, reason: 'merged_into_base' };
507
+ candidates.push(candidate);
508
+ if (!apply) continue;
509
+ const removedResult = runGit(['worktree', 'remove', wt.path], { cwd: root, check: false });
510
+ if (removedResult.status === 0) {
511
+ removed.push(candidate);
512
+ } else {
513
+ kept.push({ ...item, reason: 'remove_failed', error: (removedResult.stderr || removedResult.stdout || '').trim() });
514
+ }
515
+ }
516
+
517
+ return { apply, base, candidates, removed, kept };
518
+ }
519
+
520
+ function cleanup(args) {
521
+ const asJson = hasFlag(args, '--json');
522
+ const result = cleanupWorktrees({
523
+ root: repoRoot(),
524
+ base: readFlag(args, '--base'),
525
+ apply: hasFlag(args, '--apply'),
526
+ });
527
+ if (asJson) {
528
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
529
+ return 0;
530
+ }
531
+ const action = result.apply ? 'removed' : 'candidate';
532
+ console.log(`cleanup: ${result.apply ? 'applied' : 'dry-run'} base=${result.base}`);
533
+ console.log(`${action}s: ${result.apply ? result.removed.length : result.candidates.length}`);
534
+ for (const item of result.apply ? result.removed : result.candidates) {
535
+ console.log(`${action}: ${item.path} branch=${item.branch} reason=${item.reason}`);
536
+ }
537
+ console.log(`kept: ${result.kept.length}`);
538
+ if (!result.apply && result.candidates.length) console.log('next: atris worktree cleanup --apply');
539
+ return 0;
540
+ }
541
+
454
542
  function guide() {
455
543
  console.log('Atris worktree agent recipe');
456
544
  console.log('');
457
- console.log('1. Start isolated work:');
545
+ console.log('Default: stay in the current checkout for small, clean, single-agent fixes.');
546
+ console.log('Use a worktree for dirty launchers, parallel agents, risky edits, releases, or long proof runs.');
547
+ console.log('');
548
+ console.log('1. Start isolated work when needed:');
458
549
  console.log(' atris worktree start --member <member> --task "<task>" --claim');
459
550
  console.log(' atris worktree start --agent <agent> --task "<task>"');
460
551
  console.log('');
@@ -471,13 +562,17 @@ function guide() {
471
562
  console.log('4. Ship only from the isolated worktree:');
472
563
  console.log(' atris worktree ship --message "<commit>" --verify "<cmd>" --merge');
473
564
  console.log('');
565
+ console.log('5. Clean merged worktrees:');
566
+ console.log(' atris worktree cleanup');
567
+ console.log(' atris worktree cleanup --apply');
568
+ console.log('');
474
569
  console.log('Notes: start uses the current upstream/default remote base, not dirty local HEAD.');
475
570
  console.log('Use `atris worktree status` to see all worktrees and dirty counts.');
476
571
  return 0;
477
572
  }
478
573
 
479
574
  function help() {
480
- console.log('Usage: atris worktree <guide|start|ship|status|guard|prune>');
575
+ console.log('Usage: atris worktree <guide|start|ship|status|guard|prune|cleanup>');
481
576
  console.log('');
482
577
  console.log(' atris worktree guide');
483
578
  console.log(' atris worktree start --member <member>|--agent <name> --task "<task>" [--claim]');
@@ -485,6 +580,7 @@ function help() {
485
580
  console.log(' atris worktree status');
486
581
  console.log(' atris worktree guard [--allow-primary] [--allow-dirty]');
487
582
  console.log(' atris worktree prune [--apply]');
583
+ console.log(' atris worktree cleanup [--apply] [--json] [--base origin/master]');
488
584
  }
489
585
 
490
586
  function worktreeCommand(args = []) {
@@ -503,6 +599,7 @@ function worktreeCommand(args = []) {
503
599
  }
504
600
  if (sub === 'guard') return guard(rest);
505
601
  if (sub === 'prune') return prune(rest);
602
+ if (sub === 'cleanup' || sub === 'clean') return cleanup(rest);
506
603
  help();
507
604
  return 2;
508
605
  }
@@ -510,6 +607,7 @@ function worktreeCommand(args = []) {
510
607
  module.exports = {
511
608
  branchName,
512
609
  createAgentWorktree,
610
+ cleanupWorktrees,
513
611
  defaultShipTarget,
514
612
  defaultStartBase,
515
613
  defaultWorktreePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.30.6",
3
+ "version": "3.30.8",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",