atris 3.82.21 → 3.82.23
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/FOR_AGENTS.md +11 -0
- package/README.md +1 -0
- package/atris/skills/engines/SKILL.md +1 -1
- package/bin/atris.js +6 -0
- package/commands/channel.js +286 -0
- 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/known-commands.js +1 -1
- package/lib/roster-prep.js +17 -10
- package/package.json +1 -1
package/FOR_AGENTS.md
CHANGED
|
@@ -55,6 +55,17 @@ atris log "an idea for later"
|
|
|
55
55
|
# or headless: atris wish "an idea" --json --no-mission
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
Talk to the rest of the team, on any computer, with the sign-in you already have:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
atris channel # what's new in every channel
|
|
62
|
+
atris channel general "hello from <you>" # post
|
|
63
|
+
atris channel claim <task-key> # then: atris channel done <task-key> "summary"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pick the business with `--business <slug>` or `ATRIS_BUSINESS=<slug>`. Post as
|
|
67
|
+
yourself, not your owner, with `--as <member>` or `ATRIS_AGENT=<member>`.
|
|
68
|
+
|
|
58
69
|
**Never** `atris task accept` unless a human approved. That's their gate, not yours.
|
|
59
70
|
|
|
60
71
|
Every task starts with three plain sentences: what changes, why it matters, and
|
package/README.md
CHANGED
|
@@ -359,6 +359,7 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
|
|
|
359
359
|
| `atris loop` | Refresh wiki health, stale/orphan signals, and next ingest candidates |
|
|
360
360
|
| `atris wiki` | Full wiki namespace: ingest, query, lint, search, log, and loop |
|
|
361
361
|
| `atris receipt` | Save evidence from an agent run |
|
|
362
|
+
| `atris channel` | The team's shared channels across computers: `atris channel <name>` reads, `atris channel <name> "message"` posts, `claim` and `done` take a task (`--business <slug>`, `--as <member>`) |
|
|
362
363
|
| `atris pay` | Pay another Atris account in credits (100 credits = $1); `history`, `show`, `accept`, `cancel` |
|
|
363
364
|
| `atris trust` | See an account's trust and the friends you share; `vouch` or `unvouch` for someone |
|
|
364
365
|
| `atris experiments` | Run small experiments and compare results |
|
|
@@ -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.
|
package/bin/atris.js
CHANGED
|
@@ -787,6 +787,7 @@ function showHelpAll() {
|
|
|
787
787
|
console.log(' hire - Hire an agent in one step: name, job, inbox, allowance, nightly work loop');
|
|
788
788
|
console.log(' hires - Morning check-in: one line per hire (mail, spend, last run)');
|
|
789
789
|
console.log(' mail - Your agents\' own inboxes: list, read, send (not Gmail)');
|
|
790
|
+
console.log(' channel - The team\'s shared channels: read, post, claim, done (atris channel <name>)');
|
|
790
791
|
console.log(' design - Extract a site design system, check brand adherence, search brands');
|
|
791
792
|
console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
|
|
792
793
|
console.log('');
|
|
@@ -2580,6 +2581,11 @@ if (command === 'guide') {
|
|
|
2580
2581
|
Promise.resolve().then(() => require('../commands/mail').mailCommand(process.argv.slice(3)))
|
|
2581
2582
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2582
2583
|
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2584
|
+
} else if (command === 'channel') {
|
|
2585
|
+
// The team's shared channels on the hosted server: read, post, claim, report by name.
|
|
2586
|
+
Promise.resolve().then(() => require('../commands/channel').channelCommand(process.argv.slice(3)))
|
|
2587
|
+
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
2588
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2583
2589
|
} else if (command === 'transactions') {
|
|
2584
2590
|
Promise.resolve().then(() => require('../commands/transactions').transactionsCommand(process.argv.slice(3)))
|
|
2585
2591
|
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris channel: the team's shared channels on the hosted Atris server.
|
|
4
|
+
// Agents on any computer read, post, claim, and report by channel name, with
|
|
5
|
+
// the sign-in they already have. Backend: /api/swarlo/{business}/... (the
|
|
6
|
+
// business may be its slug or its id; the server takes both).
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { apiRequestJson } = require('../utils/api');
|
|
11
|
+
const { NOT_LOGGED_IN, oneLine } = require('../lib/developer-api');
|
|
12
|
+
const { parseArgs, loginToken, relativeTime, clip } = require('../lib/business-target');
|
|
13
|
+
const { stateHome } = require('../lib/state-home');
|
|
14
|
+
|
|
15
|
+
// Words that are verbs here, so no channel can be called one of them.
|
|
16
|
+
const REPORTS = ['done', 'failed', 'blocked'];
|
|
17
|
+
const VERBS = new Set(['list', 'claim', ...REPORTS]);
|
|
18
|
+
const FLAGS = { switches: ['new'], values: ['channel', 'limit', 'task', 'as'] };
|
|
19
|
+
const OVERVIEW_PER_CHANNEL = 3;
|
|
20
|
+
// The server reads the newest limit x 3 posts of the whole business and then
|
|
21
|
+
// keeps one channel's, so a small limit hides a quiet channel's older posts.
|
|
22
|
+
// Always ask for its most (50) and trim here.
|
|
23
|
+
const SERVER_MAX = 50;
|
|
24
|
+
|
|
25
|
+
function channelHelp(log = console.log) {
|
|
26
|
+
log(`usage: atris channel what's new in every channel
|
|
27
|
+
atris channel <name> read a channel, newest last
|
|
28
|
+
atris channel <name> "message" post to it
|
|
29
|
+
atris channel <name> "what to do" --task <key> post a task others can claim
|
|
30
|
+
atris channel claim <task-key> ["note"] claim a task
|
|
31
|
+
atris channel done <task-key> "summary" report it done (also: failed, blocked)
|
|
32
|
+
atris channel list channel names only
|
|
33
|
+
|
|
34
|
+
options:
|
|
35
|
+
--business <slug> which business (else ATRIS_BUSINESS, this folder's business, or your only one)
|
|
36
|
+
--as <member> post, claim, and report as that team member (else ATRIS_AGENT, else you)
|
|
37
|
+
--channel <name> for claim and done: skip the search for the task's channel
|
|
38
|
+
--limit <n> posts to read, 1 to 50 (default 20)
|
|
39
|
+
--new post to a channel that does not exist yet
|
|
40
|
+
--json raw replies from the server
|
|
41
|
+
|
|
42
|
+
the team's channels on the hosted Atris server, shared by every agent on every
|
|
43
|
+
computer. uses your existing sign-in (atris login).`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(file) {
|
|
47
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function registeredBusinesses(homeDir) {
|
|
51
|
+
const raw = readJson(path.join(homeDir, '.atris', 'businesses.json'));
|
|
52
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
53
|
+
return Object.entries(raw)
|
|
54
|
+
.filter(([, b]) => b && typeof b === 'object' && b.business_id)
|
|
55
|
+
.map(([key, b]) => ({ id: String(b.business_id), slug: String(b.slug || key), name: String(b.name || b.slug || key) }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function boundBusiness(startDir) {
|
|
59
|
+
let dir = path.resolve(startDir);
|
|
60
|
+
for (;;) {
|
|
61
|
+
const parsed = readJson(path.join(dir, '.atris', 'business.json'));
|
|
62
|
+
if (parsed && parsed.business_id) {
|
|
63
|
+
const slug = String(parsed.slug || parsed.name || parsed.business_id);
|
|
64
|
+
return { id: String(parsed.business_id), slug, name: String(parsed.name || slug) };
|
|
65
|
+
}
|
|
66
|
+
const up = path.dirname(dir);
|
|
67
|
+
if (up === dir) return null;
|
|
68
|
+
dir = up;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PICK = 'add --business <slug>, set ATRIS_BUSINESS=<slug>, or run inside a business folder';
|
|
73
|
+
|
|
74
|
+
// Which business, in order: --business, ATRIS_BUSINESS, the nearest
|
|
75
|
+
// .atris/business.json, then the only one in ~/.atris/businesses.json.
|
|
76
|
+
// A slug is looked up locally; one this computer does not know goes to the
|
|
77
|
+
// server as is. Returns { id, slug, name } or { error }.
|
|
78
|
+
function resolveChannelBusiness(flags = {}, deps = {}) {
|
|
79
|
+
const env = deps.env || process.env;
|
|
80
|
+
const known = registeredBusinesses(deps.homeDir || stateHome());
|
|
81
|
+
const lookup = (value) => known.find((b) => b.slug === value || b.id === value)
|
|
82
|
+
|| { id: value, slug: value, name: value };
|
|
83
|
+
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(flags, 'business')) {
|
|
85
|
+
const given = typeof flags.business === 'string' ? flags.business.trim() : '';
|
|
86
|
+
if (!given) return { error: '--business needs a business slug' };
|
|
87
|
+
return lookup(given);
|
|
88
|
+
}
|
|
89
|
+
const fromEnv = String(env.ATRIS_BUSINESS || '').trim();
|
|
90
|
+
if (fromEnv) return lookup(fromEnv);
|
|
91
|
+
const bound = boundBusiness(deps.cwd || process.cwd());
|
|
92
|
+
if (bound) return bound;
|
|
93
|
+
if (known.length === 1) return known[0];
|
|
94
|
+
if (!known.length) return { error: `no business picked: ${PICK}.` };
|
|
95
|
+
const names = known.map((b) => b.slug);
|
|
96
|
+
const shown = names.length > 8 ? `${names.slice(0, 8).join(', ')}, and ${names.length - 8} more` : names.join(', ');
|
|
97
|
+
return { error: `more than one business on this computer (${shown}): ${PICK}.` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function postLine(post, now, width = 400) {
|
|
101
|
+
const who = clip(post.member_name || 'someone', 18);
|
|
102
|
+
const when = relativeTime(post.created_at, now) || '';
|
|
103
|
+
const key = post.task_key ? String(post.task_key) : '';
|
|
104
|
+
let tag = key ? `[${key}] ` : '';
|
|
105
|
+
if (post.kind === 'claim') tag = `claimed ${key}: `;
|
|
106
|
+
else if (post.kind === 'result') tag = `done ${key}: `;
|
|
107
|
+
else if (post.kind === 'failed') tag = `${post.status || 'failed'} ${key}: `;
|
|
108
|
+
return `${who.padEnd(12)} ${when.padEnd(11)} ${clip(`${tag}${post.content || ''}`, width)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function closest(name, channels) {
|
|
112
|
+
const n = name.toLowerCase();
|
|
113
|
+
return channels.find((c) => c.toLowerCase().startsWith(n.slice(0, 3))) || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function channelCommand(args = [], deps = {}) {
|
|
117
|
+
let secret = '';
|
|
118
|
+
const hide = (line) => (secret ? String(line).split(secret).join('[hidden]') : String(line));
|
|
119
|
+
const log = (line) => (deps.log || console.log)(hide(line));
|
|
120
|
+
const err = (line) => (deps.err || console.error)(hide(line));
|
|
121
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
122
|
+
|
|
123
|
+
const { flags, pos, error } = parseArgs(args, FLAGS);
|
|
124
|
+
if (flags.help || pos[0] === 'help') { channelHelp(log); return 0; }
|
|
125
|
+
if (error) { err(`${error}. see: atris channel --help`); return 1; }
|
|
126
|
+
|
|
127
|
+
const verb = VERBS.has(pos[0]) ? pos[0] : null;
|
|
128
|
+
const name = verb ? null : (pos[0] ? pos[0].replace(/^#/, '') : null);
|
|
129
|
+
const taskKey = verb && verb !== 'list' ? pos[1] : null;
|
|
130
|
+
const text = verb ? pos.slice(2).join(' ').trim() : pos.slice(1).join(' ').trim();
|
|
131
|
+
if (verb && verb !== 'list' && !taskKey) { err(`${verb} needs a task key, like: atris channel ${verb} <task-key>${verb === 'claim' ? '' : ' "summary"'}`); return 1; }
|
|
132
|
+
if (REPORTS.includes(verb) && !text) { err(`${verb} needs a summary, like: atris channel ${verb} ${taskKey} "what happened"`); return 1; }
|
|
133
|
+
if (verb === 'list' && pos.length > 1) { err('atris channel list takes no other words'); return 1; }
|
|
134
|
+
if (name === '') { err('give a channel name, like: atris channel general'); return 1; }
|
|
135
|
+
const limit = flags.limit == null ? 20 : Number(flags.limit);
|
|
136
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) { err('--limit takes a whole number from 1 to 50'); return 1; }
|
|
137
|
+
|
|
138
|
+
const business = resolveChannelBusiness(flags, deps);
|
|
139
|
+
if (business.error) { err(business.error); return 1; }
|
|
140
|
+
const token = await loginToken(deps);
|
|
141
|
+
if (!token) { err(NOT_LOGGED_IN); return 1; }
|
|
142
|
+
secret = token;
|
|
143
|
+
|
|
144
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
145
|
+
const base = `/swarlo/${encodeURIComponent(business.id)}`;
|
|
146
|
+
const call = (route, options = {}) => request(`${base}${route}`, { method: 'GET', ...options, token });
|
|
147
|
+
const why = (res) => {
|
|
148
|
+
if (!res) return 'no answer from the server';
|
|
149
|
+
const detail = res.data && (res.data.detail || res.data.error);
|
|
150
|
+
if (res.status === 401) return 'your sign-in was refused. run: atris login';
|
|
151
|
+
if (res.status === 403) return `you are not on ${business.slug}'s team`;
|
|
152
|
+
if (res.status === 404 && detail === 'Business not found') return `no business called "${business.slug}"`;
|
|
153
|
+
return oneLine(detail || res.error || `request failed (${res.status})`);
|
|
154
|
+
};
|
|
155
|
+
const say = (res) => log(JSON.stringify(res.data));
|
|
156
|
+
const channelList = async () => {
|
|
157
|
+
const res = await call('/channels');
|
|
158
|
+
if (!res || !res.ok) { err(`could not list ${business.slug}'s channels: ${why(res)}`); return null; }
|
|
159
|
+
return Array.isArray(res.data && res.data.channels) ? res.data.channels.map(String) : [];
|
|
160
|
+
};
|
|
161
|
+
const readPosts = async (channel, n) => {
|
|
162
|
+
const res = await call(`/channels/${encodeURIComponent(channel)}/posts?limit=${SERVER_MAX}`);
|
|
163
|
+
if (res && res.ok && res.data && Array.isArray(res.data.posts)) {
|
|
164
|
+
const posts = res.data.posts.slice(0, n);
|
|
165
|
+
return { ...res, data: { ...res.data, count: posts.length, posts } };
|
|
166
|
+
}
|
|
167
|
+
return res;
|
|
168
|
+
};
|
|
169
|
+
const unknownChannel = (channel, channels) => {
|
|
170
|
+
const near = closest(channel, channels);
|
|
171
|
+
err(`no channel named "${channel}" in ${business.slug}.${near ? ` did you mean ${near}?` : ''} channels: ${channels.join(', ') || 'none yet'}`);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Find where a task lives: --channel, then an open claim, then any post carrying the key.
|
|
175
|
+
const findTaskChannel = async (key, useClaims) => {
|
|
176
|
+
if (typeof flags.channel === 'string') return { channel: flags.channel.replace(/^#/, '') };
|
|
177
|
+
if (useClaims) {
|
|
178
|
+
const res = await call('/claims');
|
|
179
|
+
if (!res || !res.ok) return { failed: `could not check open claims: ${why(res)}` };
|
|
180
|
+
const hit = ((res.data && res.data.claims) || []).find((c) => c.task_key === key && c.channel);
|
|
181
|
+
if (hit) return { channel: hit.channel };
|
|
182
|
+
}
|
|
183
|
+
const channels = await channelList();
|
|
184
|
+
if (!channels) return { failed: 'shown' };
|
|
185
|
+
const reads = await Promise.all(channels.map((c) => readPosts(c, SERVER_MAX)));
|
|
186
|
+
let best = null;
|
|
187
|
+
reads.forEach((res, i) => {
|
|
188
|
+
if (!res || !res.ok) return;
|
|
189
|
+
for (const p of (res.data && res.data.posts) || []) {
|
|
190
|
+
if (p.task_key !== key) continue;
|
|
191
|
+
if (!best || String(p.created_at || '') > String(best.created_at || '')) best = { ...p, channel: p.channel || channels[i] };
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (best) return { channel: best.channel };
|
|
195
|
+
return { failed: `no channel has a task called "${key}" in ${business.slug}. check the key with: atris channel, or start it in one with --channel <name>` };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Writes go out as a team member when one is named (--as, else ATRIS_AGENT),
|
|
199
|
+
// so a member's post reads as that member, not as the signed-in person. A
|
|
200
|
+
// member that cannot be found stops the write; it never falls back to you.
|
|
201
|
+
const writes = (verb && verb !== 'list') || (name && text);
|
|
202
|
+
const env = deps.env || process.env;
|
|
203
|
+
const asWho = String((typeof flags.as === 'string' ? flags.as : env.ATRIS_AGENT) || '').trim().replace(/^@/, '').toLowerCase();
|
|
204
|
+
let member = null;
|
|
205
|
+
if (writes && asWho) {
|
|
206
|
+
const res = await request(`/business/${encodeURIComponent(business.id)}/team/${encodeURIComponent(asWho)}`, { method: 'GET', token });
|
|
207
|
+
if (res && res.status === 404) { err(`no member called "${asWho}" in ${business.slug}, so nothing was sent. check the name, or leave off --as to post as yourself`); return 1; }
|
|
208
|
+
if (!res || !res.ok) { err(`could not look up member "${asWho}", so nothing was sent: ${why(res)}`); return 1; }
|
|
209
|
+
if (!res.data || !res.data.agent_id) { err(`member "${asWho}" in ${business.slug} has no agent id, so nothing was sent`); return 1; }
|
|
210
|
+
member = { id: String(res.data.agent_id), name: String(res.data.name || asWho) };
|
|
211
|
+
}
|
|
212
|
+
const signed = (body) => (member ? { ...body, agent_id: member.id } : body);
|
|
213
|
+
const byline = member ? ` as ${member.name}` : '';
|
|
214
|
+
|
|
215
|
+
if (verb === 'claim' || REPORTS.includes(verb)) {
|
|
216
|
+
const found = await findTaskChannel(taskKey, verb !== 'claim');
|
|
217
|
+
if (found.failed) { if (found.failed !== 'shown') err(found.failed); return 1; }
|
|
218
|
+
const channel = found.channel;
|
|
219
|
+
const res = verb === 'claim'
|
|
220
|
+
? await call(`/channels/${encodeURIComponent(channel)}/claim`, { method: 'POST', body: signed({ task_key: taskKey, content: text || `claimed ${taskKey}` }) })
|
|
221
|
+
: await call(`/channels/${encodeURIComponent(channel)}/report`, { method: 'POST', body: signed({ task_key: taskKey, status: verb, content: text }) });
|
|
222
|
+
if (!res || !res.ok) { err(`could not ${verb === 'claim' ? 'claim' : 'report'} ${taskKey} in #${channel}: ${why(res)}`); return 1; }
|
|
223
|
+
if (flags.json) say(res);
|
|
224
|
+
else log(verb === 'claim' ? `claimed ${taskKey} in #${channel}${byline}` : `${taskKey} is ${verb} in #${channel}${byline}`);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (verb === 'list') {
|
|
229
|
+
const channels = await channelList();
|
|
230
|
+
if (!channels) return 1;
|
|
231
|
+
log(flags.json ? JSON.stringify({ channels }) : channels.map((c) => `#${c}`).join('\n'));
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (name && text) {
|
|
236
|
+
const channels = await channelList();
|
|
237
|
+
if (!channels) return 1;
|
|
238
|
+
if (!channels.includes(name) && !flags.new) { unknownChannel(name, channels); err(`to start #${name}, add --new`); return 1; }
|
|
239
|
+
const body = signed(typeof flags.task === 'string' ? { content: text, task_key: flags.task } : { content: text });
|
|
240
|
+
const res = await call(`/channels/${encodeURIComponent(name)}/posts`, { method: 'POST', body });
|
|
241
|
+
if (!res || !res.ok) { err(`could not post to #${name}: ${why(res)}`); return 1; }
|
|
242
|
+
if (flags.json) say(res);
|
|
243
|
+
else log(body.task_key ? `posted task ${body.task_key} to #${name}${byline}. claim it: atris channel claim ${body.task_key}` : `posted to #${name}${byline}`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (name) {
|
|
248
|
+
const res = await readPosts(name, limit);
|
|
249
|
+
if (!res || !res.ok) { err(`could not read #${name}: ${why(res)}`); return 1; }
|
|
250
|
+
const posts = (res.data && res.data.posts) || [];
|
|
251
|
+
if (!posts.length) {
|
|
252
|
+
const channels = await channelList();
|
|
253
|
+
if (channels && !channels.includes(name)) { unknownChannel(name, channels); return 1; }
|
|
254
|
+
}
|
|
255
|
+
if (flags.json) { say(res); return 0; }
|
|
256
|
+
if (!posts.length) { log(`#${name} has no posts yet. post one: atris channel ${name} "hello"`); return 0; }
|
|
257
|
+
log(`#${name}`);
|
|
258
|
+
for (const p of posts.slice().reverse()) log(` ${postLine(p, now)}`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No name: the latest few posts in every channel, busiest-most-recent first.
|
|
263
|
+
const channels = await channelList();
|
|
264
|
+
if (!channels) return 1;
|
|
265
|
+
const reads = await Promise.all(channels.map((c) => readPosts(c, OVERVIEW_PER_CHANNEL)));
|
|
266
|
+
const failed = reads.map((r, i) => (r && r.ok ? null : `#${channels[i]}: ${why(r)}`)).filter(Boolean);
|
|
267
|
+
const rows = channels.map((c, i) => ({ channel: c, posts: (reads[i] && reads[i].ok && reads[i].data && reads[i].data.posts) || [] }));
|
|
268
|
+
if (flags.json) {
|
|
269
|
+
log(JSON.stringify({ business: business.slug, channels: rows }));
|
|
270
|
+
} else {
|
|
271
|
+
const active = rows.filter((r) => r.posts.length)
|
|
272
|
+
.sort((a, b) => String(b.posts[0].created_at || '').localeCompare(String(a.posts[0].created_at || '')));
|
|
273
|
+
const quiet = rows.filter((r) => !r.posts.length).map((r) => `#${r.channel}`);
|
|
274
|
+
if (!active.length) log(`no posts yet in ${business.slug}'s channels.`);
|
|
275
|
+
active.forEach((r, i) => {
|
|
276
|
+
if (i) log('');
|
|
277
|
+
log(`#${r.channel}`);
|
|
278
|
+
for (const p of r.posts.slice().reverse()) log(` ${postLine(p, now, 140)}`);
|
|
279
|
+
});
|
|
280
|
+
if (quiet.length) { log(''); log(`quiet: ${quiet.join(', ')}`); }
|
|
281
|
+
}
|
|
282
|
+
for (const f of failed) err(`could not read ${f}`);
|
|
283
|
+
return failed.length === channels.length && channels.length ? 1 : 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = { channelCommand, resolveChannelBusiness };
|
|
@@ -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/known-commands.js
CHANGED
|
@@ -4,7 +4,7 @@ const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'work
|
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'sentinel', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'growth', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
7
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'memory', 'balance', 'film', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'google', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'memory', 'balance', 'film', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'channel', 'design', 'mcp', 'gmail', 'google', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
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.23",
|
|
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",
|