atris 3.82.21 → 3.82.22

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.
@@ -113,7 +113,7 @@ Smoke: `reply with the word OK`, run from `$HOME`, 120s cap, one at a time on a
113
113
  | Devin (search) | `devin -p --permission-mode dangerous --model swe-1.7-lightning -- "<prompt>"` | `swe-1.7-lightning` (paid, no Free tag on 2026-09-23) | 2026-09-23 | 13.9 | |
114
114
  | Grok | `grok --always-approve --model grok-4.7 -p "<prompt>"` (run from the target repo) | `grok-4.7` | 2026-09-23 | 11.7 | Unpinned default is now `grok-4.7-build-fast`; `grok models` lists the menu. Uses grok.com login. `--best-of-n <N>` for tricky bounded builds. |
115
115
  | Muse | `muse exec --sandbox-network enabled --trust-workspace --model muse-wonder-1.0 "<prompt>"` (run from the target repo) | `muse-wonder-1.0` | 2026-11-11 | - | Meta's Muse Code. Runs only Muse models (`muse wonder`, `muse spark`). Its sandbox proxies the network by default, which blocks atris calls; `enabled` keeps the sandbox and approval judge on. Read-only asks add `--disable-write`. |
116
- | Antigravity (agy) | `agy --mode accept-edits --add-dir "$PWD" --model gemini-3.8-flash-high --output-format json -p "<prompt>"` (run from the target repo) | `gemini-3.8-flash-high` | 2026-09-23 | 15.9 | **`--add-dir` is mandatory for writes**: without it agy edits `~/.gemini/antigravity-cli/scratch/` and the project never changes (2026-08-28). `--mode plan --sandbox` for read-only review. Also answers to "gemini". |
116
+ | Antigravity (agy) | `agy --mode accept-edits --add-dir "$PWD" --model gemini-3.8-flash-high --output-format json -p "<prompt>"` (run from the target repo) | `gemini-3.8-flash-high` | 2026-09-23 | 15.9 | **`--add-dir` is mandatory for writes**: without it agy edits `~/.gemini/antigravity-cli/scratch/` and the project never changes (2026-08-28). For questions use `atris engine ask --engine agy`: it runs agy in edit mode, sandboxed, from an empty throwaway folder (plan mode hangs with no output), and picks `gemini-4-carbon-medium` when no model is named, since High is too slow for questions. Put any repo context in the question itself. Also answers to "gemini". |
117
117
  | opencode | `opencode run -m opencode/muse-spark-1.3-contributor-free "<prompt>"` (read-only: add `--agent plan`) | `opencode/muse-spark-1.3-contributor-free` | 2026-09-23 | 41.1 | Build work needs `--auto` (isolated worktree only). `opencode models` lists the live menu. |
118
118
 
119
119
  Headless dispatch permissions (verified 2026-08-11): `codex exec`, `grok`, and `cursor-agent` are allowlisted in `~/.claude/settings.json` so fresh and one-shot sessions can dispatch without a human approval click. A cold session that gets "requires approval" on an engine command means that allowlist regressed.
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ // The orb, in the terminal: the same chat as atris.ai/orb, with its numbered choices.
4
+ // atris orb chat talk, type a number to pick a choice or write your own
5
+ // atris orb say "text" one message, prints the reply and numbered choices
6
+ // atris orb pick 2 picks choice 2 from the last reply
7
+ // atris orb new starts fresh with the orb's own opener
8
+ // The thread lives in ~/.atris/orb-chat.json so say and pick continue the same chat.
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const readline = require('node:readline');
13
+ const { stateHome } = require('../lib/state-home');
14
+ const { getApiBaseUrl } = require('../utils/api');
15
+
16
+ const MAX_MESSAGES = 16;
17
+ const MAX_CHOICE_SETS = 6;
18
+ const OPENER_MESSAGE = 'Open my day.';
19
+
20
+ const TOOL_WORDS = {
21
+ computer_list: 'looking at your computers',
22
+ computer_team: 'looking at your helpers',
23
+ computer_execute: 'sending the work to your computer',
24
+ computer_delegate: 'handing the work to a helper',
25
+ computer_inflight: 'checking what your computers are doing',
26
+ computer_execution_result: 'reading what a job found',
27
+ web_search: 'searching the web',
28
+ memory_search: 'searching your notes',
29
+ memory_read: 'reading your notes',
30
+ memory_ask: 'reading your notes',
31
+ memory_write: 'writing a page',
32
+ remember: 'saving that',
33
+ };
34
+
35
+ function stateFile() {
36
+ return path.join(stateHome(), '.atris', 'orb-chat.json');
37
+ }
38
+
39
+ function readState() {
40
+ try {
41
+ const parsed = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
42
+ if (parsed && Array.isArray(parsed.messages)) return parsed;
43
+ } catch {}
44
+ return { messages: [], choiceHistory: [], options: [] };
45
+ }
46
+
47
+ function writeState(state) {
48
+ const file = stateFile();
49
+ fs.mkdirSync(path.dirname(file), { recursive: true });
50
+ const trimmed = {
51
+ messages: state.messages.slice(-MAX_MESSAGES),
52
+ choiceHistory: state.choiceHistory.slice(-MAX_CHOICE_SETS),
53
+ options: state.options || [],
54
+ };
55
+ fs.writeFileSync(file, JSON.stringify(trimmed, null, 2), { mode: 0o600 });
56
+ }
57
+
58
+ function token(env = process.env) {
59
+ if (env.ATRIS_TOKEN) return env.ATRIS_TOKEN;
60
+ try {
61
+ const creds = require('../utils/auth').loadCredentials();
62
+ return creds && creds.token ? String(creds.token) : '';
63
+ } catch {
64
+ return '';
65
+ }
66
+ }
67
+
68
+ function turnBody(state, message, { opener = false } = {}) {
69
+ const body = {
70
+ message,
71
+ surface: 'dashboard_orb',
72
+ model: 'atris:fast',
73
+ memory_enabled: true,
74
+ max_turns: 4,
75
+ previous_messages: opener ? [] : state.messages.slice(-MAX_MESSAGES),
76
+ choice_history: state.choiceHistory.slice(-MAX_CHOICE_SETS),
77
+ };
78
+ if (opener) {
79
+ Object.assign(body, {
80
+ orb_opener: true,
81
+ turn_kind: 'orb_open',
82
+ disable_connector_tools: true,
83
+ max_turns: 1,
84
+ });
85
+ }
86
+ return body;
87
+ }
88
+
89
+ function parseSseLines(buffer, onEvent) {
90
+ const lines = buffer.split('\n');
91
+ const rest = lines.pop();
92
+ for (const raw of lines) {
93
+ const line = raw.trim();
94
+ if (!line.startsWith('data:')) continue;
95
+ const data = line.slice(5).trim();
96
+ if (!data || data === '[DONE]') continue;
97
+ try { onEvent(JSON.parse(data)); } catch {}
98
+ }
99
+ return rest;
100
+ }
101
+
102
+ async function sendTurn(state, message, { opener = false, out = process.stdout, fetchImpl = globalThis.fetch } = {}) {
103
+ const auth = token();
104
+ if (!auth) throw new Error('Not signed in. Run: atris login');
105
+ const response = await fetchImpl(`${getApiBaseUrl()}/atris2/turn`, {
106
+ method: 'POST',
107
+ headers: {
108
+ Authorization: `Bearer ${auth}`,
109
+ 'Content-Type': 'application/json',
110
+ Accept: 'text/event-stream',
111
+ },
112
+ body: JSON.stringify(turnBody(state, message, { opener })),
113
+ });
114
+ if (!response.ok) {
115
+ const detail = await response.text().catch(() => '');
116
+ throw new Error(`The orb did not answer (${response.status}). ${detail.slice(0, 200)}`.trim());
117
+ }
118
+
119
+ let text = '';
120
+ let options = [];
121
+ let lastToolWords = '';
122
+ const onEvent = (event) => {
123
+ if (event.type === 'text_delta' && event.content) {
124
+ text += event.content;
125
+ out.write(event.content);
126
+ } else if (event.type === 'options' && Array.isArray(event.options)) {
127
+ options = event.options
128
+ .filter((option) => option && option.label)
129
+ .map((option) => ({ label: String(option.label), message: String(option.message || option.label) }));
130
+ } else if (event.type === 'tool_call') {
131
+ const words = TOOL_WORDS[event.tool];
132
+ if (words && words !== lastToolWords) {
133
+ lastToolWords = words;
134
+ out.write(`${text && !text.endsWith('\n') ? '\n' : ''}\x1b[2m· ${words}\x1b[0m\n`);
135
+ }
136
+ } else if (event.type === 'error' && event.error) {
137
+ out.write(`\n\x1b[2m(${String(event.error).slice(0, 160)})\x1b[0m\n`);
138
+ }
139
+ };
140
+
141
+ const decoder = new TextDecoder();
142
+ let buffer = '';
143
+ for await (const chunk of response.body) {
144
+ buffer += decoder.decode(chunk, { stream: true });
145
+ buffer = parseSseLines(buffer, onEvent);
146
+ }
147
+ parseSseLines(`${buffer}\n`, onEvent);
148
+ if (!text.endsWith('\n')) out.write('\n');
149
+
150
+ if (!opener) state.messages.push({ role: 'user', content: message });
151
+ if (text.trim()) state.messages.push({ role: 'assistant', content: text.trim() });
152
+ state.options = options;
153
+ if (options.length) state.choiceHistory.push({ shown: options.map((option) => option.label), picked: null });
154
+ writeState(state);
155
+ return { text, options };
156
+ }
157
+
158
+ function renderOptions(options, out = process.stdout) {
159
+ if (!options.length) return;
160
+ out.write('\n');
161
+ options.forEach((option, index) => {
162
+ out.write(` \x1b[1m${index + 1}\x1b[0m ${option.label}\n`);
163
+ });
164
+ }
165
+
166
+ // A number picks a choice from the last reply; anything else is their own words.
167
+ function resolveInput(state, input) {
168
+ const trimmed = String(input || '').trim();
169
+ const number = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
170
+ const options = state.options || [];
171
+ if (Number.isInteger(number) && number >= 1 && number <= options.length) {
172
+ const option = options[number - 1];
173
+ const lastSet = state.choiceHistory[state.choiceHistory.length - 1];
174
+ if (lastSet) lastSet.picked = option.label;
175
+ return { message: option.message, echo: option.label };
176
+ }
177
+ return { message: trimmed, echo: '' };
178
+ }
179
+
180
+ async function say(state, input, io) {
181
+ const { message, echo } = resolveInput(state, input);
182
+ if (!message) return;
183
+ if (echo) io.out.write(`\x1b[2m> ${echo}\x1b[0m\n`);
184
+ io.out.write('\n');
185
+ const { options } = await sendTurn(state, message, io);
186
+ renderOptions(options, io.out);
187
+ }
188
+
189
+ async function openFresh(io) {
190
+ const state = { messages: [], choiceHistory: readState().choiceHistory, options: [] };
191
+ const { options } = await sendTurn(state, OPENER_MESSAGE, { ...io, opener: true });
192
+ renderOptions(options, io.out);
193
+ return state;
194
+ }
195
+
196
+ async function interactive(io) {
197
+ let state = await openFresh(io);
198
+ const rl = readline.createInterface({ input: process.stdin, output: io.out });
199
+ const ask = () => new Promise((resolve) => rl.question('\n\x1b[2mpick a number or say anything (q to leave)\x1b[0m\n> ', resolve));
200
+ try {
201
+ for (;;) {
202
+ const input = (await ask()).trim();
203
+ if (!input) continue;
204
+ if (/^(q|quit|exit|bye)$/i.test(input)) break;
205
+ if (/^new$/i.test(input)) { state = await openFresh(io); continue; }
206
+ try {
207
+ await say(state, input, io);
208
+ } catch (error) {
209
+ io.out.write(`\n${error.message || error}\n`);
210
+ }
211
+ }
212
+ } finally {
213
+ rl.close();
214
+ }
215
+ return 0;
216
+ }
217
+
218
+ async function runOrbChat(args = [], io = { out: process.stdout }) {
219
+ const [verb, ...rest] = args;
220
+ if (verb === 'new') {
221
+ await openFresh(io);
222
+ return 0;
223
+ }
224
+ if (verb === 'say') {
225
+ const text = rest.join(' ').trim();
226
+ if (!text) {
227
+ io.out.write('Usage: atris orb say "what is on your mind"\n');
228
+ return 2;
229
+ }
230
+ await say(readState(), text, io);
231
+ return 0;
232
+ }
233
+ if (verb === 'pick') {
234
+ const state = readState();
235
+ if (!state.options.length) {
236
+ io.out.write('No choices yet. Start with: atris orb new\n');
237
+ return 2;
238
+ }
239
+ const number = Number(rest[0]);
240
+ if (!Number.isInteger(number) || number < 1 || number > state.options.length) {
241
+ io.out.write(`Pick a number from 1 to ${state.options.length}.\n`);
242
+ return 2;
243
+ }
244
+ await say(state, String(number), io);
245
+ return 0;
246
+ }
247
+ return interactive(io);
248
+ }
249
+
250
+ module.exports = { runOrbChat, resolveInput, turnBody, parseSseLines, readState, writeState, stateFile };
package/commands/orb.js CHANGED
@@ -89,6 +89,7 @@ function showHelp() {
89
89
  console.log('Usage: atris orb [--once] [--json] [--engine claude|codex|fast]');
90
90
  console.log(' atris orb <n> | atris orb --pick <n>');
91
91
  console.log(' atris orb scorecard [--days N]');
92
+ console.log(' atris orb chat | atris orb new | atris orb say "text" | atris orb pick <n>');
92
93
  console.log('');
93
94
  console.log('Choose a next move while background engine jobs keep working.');
94
95
  console.log('--json emits {moves:[{n,label,command}]} and exits.');
@@ -162,6 +163,11 @@ async function runOrb(args = []) {
162
163
  return 0;
163
164
  }
164
165
 
166
+ if (['chat', 'say', 'pick', 'new'].includes(args[0])) {
167
+ const { runOrbChat } = require('./orb-chat');
168
+ return runOrbChat(args[0] === 'chat' ? args.slice(1) : args);
169
+ }
170
+
165
171
  if (args[0] === 'scorecard') {
166
172
  const parsed = parseOrbScorecardDays(args.slice(1));
167
173
  if (!parsed.ok) {
package/lib/engine-ask.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  canonicalEngineName,
15
15
  claudeModelId,
16
16
  currentClaudeModel,
17
+ DEFAULT_AGY_ASK_MODEL,
17
18
  engineFailureHealthStatus,
18
19
  setEngineHealth,
19
20
  } = require('./engine-registry');
@@ -252,6 +253,8 @@ function resolveAskModel(engine, modelName = '') {
252
253
  ? claudeModelId(requested)
253
254
  : currentClaudeModel(requested);
254
255
  }
256
+ // agy asks name their default so the receipt says which model answered.
257
+ if (engine === 'agy') return DEFAULT_AGY_ASK_MODEL;
255
258
  if (engine !== 'claude' && engine !== 'fable' && engine !== 'haiku') return '';
256
259
  // No model named: this is the unattended default, so it goes through the
257
260
  // spend gate. A model a person typed is their own yes.
@@ -334,19 +337,21 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
334
337
  };
335
338
  }
336
339
  if (engine === 'agy') {
337
- // agy prompts for tool permission even in plan mode and a headless ask
338
- // has no one to answer, so it denies itself `ls` and fails every ask
339
- // (verified live 2026-08-19). The sandbox keeps the run read-only while
340
- // skip-permissions lets sandboxed reads proceed unattended.
340
+ // Plan mode hangs headless: a one-line ask timed out at 60s and at 300s,
341
+ // while edit mode answered in about 10s (measured 2026-11-11). So the ask
342
+ // runs in edit mode from an empty folder made for it (scratchCwd), where
343
+ // there is nothing real to edit; the prompt carries any repo context.
344
+ // No skip-permissions: edits land only in that folder, and anything else
345
+ // that would need a yes is refused instead of auto-approved.
341
346
  return {
342
347
  engine,
343
348
  bin: profile.bin,
349
+ scratchCwd: true,
344
350
  args: [
345
- '--mode', 'plan',
351
+ '--mode', 'accept-edits',
346
352
  '--sandbox',
347
- '--dangerously-skip-permissions',
348
353
  '--print-timeout', `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`,
349
- ...(model ? ['--model', model] : []),
354
+ '--model', model || DEFAULT_AGY_ASK_MODEL,
350
355
  ...effortArgs,
351
356
  '-p', request,
352
357
  ],
@@ -422,6 +427,20 @@ function buildAskSpawnEnv(sourceEnv = process.env) {
422
427
  return env;
423
428
  }
424
429
 
430
+ // An empty folder made for one ask, for engines whose ask runs in edit mode
431
+ // (scratchCwd). The engine starts there instead of the repo, and the folder
432
+ // is removed when the ask ends. Returns null for every other engine.
433
+ function openAskScratch(invocation) {
434
+ if (!invocation || !invocation.scratchCwd) return null;
435
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `atris-ask-${invocation.engine || 'engine'}-`));
436
+ return {
437
+ cwd: dir,
438
+ close() {
439
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
440
+ },
441
+ };
442
+ }
443
+
425
444
  function runAskProcess(invocation, {
426
445
  cwd = process.cwd(),
427
446
  timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
@@ -433,6 +452,7 @@ function runAskProcess(invocation, {
433
452
  } = {}) {
434
453
  return new Promise((resolve) => {
435
454
  const startedAt = Date.now();
455
+ let scratch = null;
436
456
  const stdoutChunks = [];
437
457
  const stderrChunks = [];
438
458
  const stdoutState = { bytes: 0, truncated: false };
@@ -449,6 +469,7 @@ function runAskProcess(invocation, {
449
469
  const finish = (code, closeSignalValue, spawnError) => {
450
470
  if (finished) return;
451
471
  finished = true;
472
+ if (scratch) scratch.close();
452
473
  clearTimeout(timeoutTimer);
453
474
  if (hardStopTimer) clearTimeout(hardStopTimer);
454
475
  if (signal && typeof signal.removeEventListener === 'function') signal.removeEventListener('abort', onAbort);
@@ -515,8 +536,9 @@ function runAskProcess(invocation, {
515
536
  }
516
537
 
517
538
  try {
539
+ scratch = openAskScratch(invocation);
518
540
  child = spawnProcess(invocation.bin, invocation.args, {
519
- cwd,
541
+ cwd: scratch ? scratch.cwd : cwd,
520
542
  env: buildAskSpawnEnv(env || invocation.env || process.env),
521
543
  detached: process.platform !== 'win32',
522
544
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -882,6 +904,7 @@ module.exports = {
882
904
  MAX_ASK_PROMPT_BYTES,
883
905
  parseEngineAskArgs,
884
906
  buildReadOnlyEngineInvocation,
907
+ openAskScratch,
885
908
  buildAskSpawnEnv,
886
909
  runAskProcess,
887
910
  runEngineAskJobs,
@@ -158,6 +158,9 @@ function engineFailureHealthStatus(result) {
158
158
  // (not build, see hostedFallbacks), and only for someone logged in to atris.
159
159
  // role_order: the engine's place for one job when it differs from its
160
160
  // fallback_order. Haiku searches first wherever the claude CLI is installed.
161
+ // An agy ask with no model named runs here: fast enough for a question,
162
+ // where High is too slow (see the agy models list below).
163
+ const DEFAULT_AGY_ASK_MODEL = 'gemini-4-carbon-medium';
161
164
  const ENGINE_SEED_META = Object.freeze({
162
165
  'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), models: Object.freeze(['atris fast']), duty: 'learning', fallback_order: 10, hosted: true }),
163
166
  // Test model on a capped free tier: a roster pick may use it, the router never hands it work.
@@ -191,6 +194,11 @@ const ENGINE_SEED_META = Object.freeze({
191
194
  // Matches `agy models` on agy 1.3.4, verified 2026-10-13.
192
195
  'gemini-4-argon-high',
193
196
  'gemini-4-argon-medium',
197
+ // Carbon on `agy models`, 2026-11-11. Medium is the ask default:
198
+ // High timed out at 150s on a 150-word question Medium answered in 25s.
199
+ 'gemini-4-carbon-low',
200
+ DEFAULT_AGY_ASK_MODEL,
201
+ 'gemini-4-carbon-high',
194
202
  'gemini-3.8-flash-high',
195
203
  'claude-opus-5-5-high',
196
204
  'claude-sonnet-5-5-high',
@@ -2491,6 +2499,7 @@ function engineDoctorReport(root = process.cwd()) {
2491
2499
  }
2492
2500
 
2493
2501
  module.exports = {
2502
+ DEFAULT_AGY_ASK_MODEL,
2494
2503
  ENGINE_ROLES,
2495
2504
  ENGINE_JOBS,
2496
2505
  SMALL_BUILD_JOB,
@@ -17,7 +17,7 @@ const {
17
17
  resolveJobTeam,
18
18
  } = require('./engine-registry');
19
19
  const { spawnSync } = require('child_process');
20
- const { buildReadOnlyEngineInvocation, buildAskSpawnEnv, runAskProcess } = require('./engine-ask');
20
+ const { buildReadOnlyEngineInvocation, buildAskSpawnEnv, openAskScratch, runAskProcess } = require('./engine-ask');
21
21
  const { appendRosterRun, parseRunUsage } = require('./roster-runs');
22
22
  const { startEngineRun } = require('./engine-resource');
23
23
 
@@ -136,15 +136,22 @@ async function defaultPrepAsk({ engine, model, effort, prompt, cwd, timeoutMs })
136
136
  // run synchronously). Same command, same answer shape as runAskProcess.
137
137
  function defaultPrepAskSync({ engine, model, effort, prompt, cwd, timeoutMs }) {
138
138
  const invocation = buildReadOnlyEngineInvocation(engine, prompt, model, { timeoutMs, effort });
139
- const run = spawnSync(invocation.bin, invocation.args, {
140
- cwd,
141
- env: buildAskSpawnEnv(),
142
- encoding: 'utf8',
143
- timeout: timeoutMs,
144
- killSignal: 'SIGKILL',
145
- maxBuffer: 4 * 1024 * 1024,
146
- stdio: ['ignore', 'pipe', 'pipe'],
147
- });
139
+ // An edit-mode ask (agy) never starts in the checkout, same as runAskProcess.
140
+ const scratch = openAskScratch(invocation);
141
+ let run;
142
+ try {
143
+ run = spawnSync(invocation.bin, invocation.args, {
144
+ cwd: scratch ? scratch.cwd : cwd,
145
+ env: buildAskSpawnEnv(),
146
+ encoding: 'utf8',
147
+ timeout: timeoutMs,
148
+ killSignal: 'SIGKILL',
149
+ maxBuffer: 4 * 1024 * 1024,
150
+ stdio: ['ignore', 'pipe', 'pipe'],
151
+ });
152
+ } finally {
153
+ if (scratch) scratch.close();
154
+ }
148
155
  const stdout = String(run.stdout || '');
149
156
  const stderr = String(run.stderr || '');
150
157
  const timedOut = Boolean(run.error && run.error.code === 'ETIMEDOUT');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.82.21",
3
+ "version": "3.82.22",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof. project memory your coding agents share across sessions.",
5
5
  "mcpName": "io.github.atrislabs/atris",
6
6
  "main": "bin/atris.js",