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.
- package/atris/skills/engines/SKILL.md +1 -1
- package/commands/orb-chat.js +250 -0
- package/commands/orb.js +6 -0
- package/lib/engine-ask.js +31 -8
- package/lib/engine-registry.js +9 -0
- package/lib/roster-prep.js +17 -10
- package/package.json +1 -1
|
@@ -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).
|
|
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
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
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', '
|
|
351
|
+
'--mode', 'accept-edits',
|
|
346
352
|
'--sandbox',
|
|
347
|
-
'--dangerously-skip-permissions',
|
|
348
353
|
'--print-timeout', `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`,
|
|
349
|
-
|
|
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,
|
package/lib/engine-registry.js
CHANGED
|
@@ -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,
|
package/lib/roster-prep.js
CHANGED
|
@@ -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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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",
|