atris 3.64.0 → 3.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/bin/atris.js +17 -0
- package/commands/agent-spawn.js +1 -0
- package/commands/engine.js +181 -63
- package/commands/hire.js +189 -0
- package/commands/mail.js +163 -0
- package/commands/mission.js +3 -2
- package/commands/stream.js +22 -1
- package/commands/team.js +355 -67
- package/lib/auto-accept-certified.js +83 -9
- package/lib/business-target.js +131 -0
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +16 -3
- package/lib/known-commands.js +1 -1
- package/lib/member-identity.js +56 -0
- package/lib/roster-models.js +11 -0
- package/lib/roster-stale.js +241 -0
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/package.json +1 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Who is working right now: engine runs alive on this machine, read from one
|
|
4
|
+
// `ps` call. A run belongs to a member when its prompt says "acting as
|
|
5
|
+
// <member>" (the dispatch convention); otherwise it shows as unattached.
|
|
6
|
+
// Nothing here starts or stops anything.
|
|
7
|
+
//
|
|
8
|
+
// ps joins a launch's words with spaces, so where the prompt starts and ends
|
|
9
|
+
// is a guess. The rules: a known engine program is always a row unless it is
|
|
10
|
+
// plainly an interactive session; settings are read only where atris's own
|
|
11
|
+
// launchers put them (test/roster-team-tables.test.js builds every launch
|
|
12
|
+
// from those launchers, so a new flag that breaks this fails a test); and
|
|
13
|
+
// when the words cannot be read with confidence the model shows as unknown
|
|
14
|
+
// rather than a guess.
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const { engineModelText } = require('./roster-models');
|
|
20
|
+
const { EFFORT_WORDS } = require('./runner-command');
|
|
21
|
+
const { readMemberLaunch } = require('./member-identity');
|
|
22
|
+
|
|
23
|
+
const PS_ARGS = ['-axo', 'pid=,ppid=,etime=,command='];
|
|
24
|
+
|
|
25
|
+
// Each engine CLI's settings: the flags that take the next word as a value
|
|
26
|
+
// and the flags that stand alone. A flag in neither list makes the reading
|
|
27
|
+
// unsure.
|
|
28
|
+
const SHARED_VALUES = ['-m', '--model', '--effort', '--add-dir', '--prompt-file', '--output-format'];
|
|
29
|
+
const SHARED_SWITCHES = ['-p', '--print', '--help', '-h', '--version', '--verbose', '--json'];
|
|
30
|
+
|
|
31
|
+
const ENGINE_FLAGS = {
|
|
32
|
+
codex: {
|
|
33
|
+
values: ['-c', '--config', '-s', '--sandbox', '-o', '--output-last-message', '-C', '--cd', '-p', '--profile',
|
|
34
|
+
'-i', '--image', '--color', '--output-schema', '-a', '--ask-for-approval', '--enable', '--disable'],
|
|
35
|
+
switches: ['--ephemeral', '--ignore-user-config', '--ignore-rules', '--dangerously-bypass-approvals-and-sandbox',
|
|
36
|
+
'--skip-git-repo-check', '--full-auto', '--oss'],
|
|
37
|
+
},
|
|
38
|
+
claude: {
|
|
39
|
+
values: ['--tools', '--permission-mode', '--input-format', '--allowedTools', '--allowed-tools', '--disallowedTools',
|
|
40
|
+
'--disallowed-tools', '--append-system-prompt', '--system-prompt', '--session-id', '--resume', '-r', '--max-turns',
|
|
41
|
+
'--mcp-config', '--settings', '--agents', '--fallback-model'],
|
|
42
|
+
switches: ['--no-session-persistence', '--dangerously-skip-permissions', '--safe-mode', '--include-partial-messages',
|
|
43
|
+
'--strict-mcp-config', '-c', '--continue'],
|
|
44
|
+
},
|
|
45
|
+
cursor: {
|
|
46
|
+
values: ['--mode', '--sandbox', '--api-key', '--resume'],
|
|
47
|
+
switches: ['--trust', '--force', '-f', '--stream-partial-output'],
|
|
48
|
+
},
|
|
49
|
+
devin: {
|
|
50
|
+
values: ['--permission-mode', '--respect-workspace-trust'],
|
|
51
|
+
switches: ['--sandbox'],
|
|
52
|
+
},
|
|
53
|
+
grok: {
|
|
54
|
+
values: ['--permission-mode', '--sandbox', '--reasoning-effort', '--best-of-n'],
|
|
55
|
+
switches: ['--always-approve', '--no-memory', '--no-subagents'],
|
|
56
|
+
},
|
|
57
|
+
agy: {
|
|
58
|
+
values: ['--mode', '--print-timeout'],
|
|
59
|
+
switches: ['--sandbox', '--dangerously-skip-permissions'],
|
|
60
|
+
},
|
|
61
|
+
droid: {
|
|
62
|
+
values: ['--reasoning-effort', '--append-system-prompt', '--cwd'],
|
|
63
|
+
switches: ['--auto', '--skip-permissions-unsafe'],
|
|
64
|
+
},
|
|
65
|
+
opencode: {
|
|
66
|
+
values: ['--variant', '--agent', '-s', '--session', '--format', '-f', '--file'],
|
|
67
|
+
switches: ['--auto'],
|
|
68
|
+
},
|
|
69
|
+
'atris-fast': {
|
|
70
|
+
values: ['--business', '--verify', '--approve', '--deny', '--grant', '--revoke-grant'],
|
|
71
|
+
switches: ['--auto', '--max', '--pro', '--fast', '--rapid', '--alpha', '--code-fast', '--local', '--cloud', '--chat',
|
|
72
|
+
'--doctor', '--approvals', '--grants', '--sync-grants', '--auto-approve', '--self-test', '--benchmark'],
|
|
73
|
+
},
|
|
74
|
+
commandcode: {
|
|
75
|
+
values: [],
|
|
76
|
+
switches: ['--yolo', '--trust'],
|
|
77
|
+
},
|
|
78
|
+
atris: {
|
|
79
|
+
values: ['--engine', '--member', '--minutes', '--job', '--timeout', '--jobs', '--scope'],
|
|
80
|
+
switches: ['--personal'],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
for (const spec of Object.values(ENGINE_FLAGS)) {
|
|
84
|
+
spec.values = new Set([...SHARED_VALUES, ...spec.values]);
|
|
85
|
+
spec.switches = new Set([...SHARED_SWITCHES, ...spec.switches]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const AX_CONTROL_FLAGS = new Set([
|
|
89
|
+
'--chat', '--doctor', '--approvals', '--approve', '--deny', '--grant', '--grants', '--sync-grants',
|
|
90
|
+
'--revoke-grant', '--auto-approve', '--self-test', '--benchmark', '--help', '-h', '--version',
|
|
91
|
+
]);
|
|
92
|
+
const ATRIS_RUN_VERBS = ['ask', 'dispatch', 'validate', 'bench', 'test'];
|
|
93
|
+
|
|
94
|
+
const printing = ({ flags }) => hasFlag(flags, ['-p', '--print']);
|
|
95
|
+
|
|
96
|
+
// What makes a launch a run. Where an interactive session looks different
|
|
97
|
+
// from a headless one (codex exec, claude -p) the difference is required;
|
|
98
|
+
// anywhere else a launch with work in it is a run. A string names the engine
|
|
99
|
+
// when the launcher is atris itself.
|
|
100
|
+
const RUN_RULES = {
|
|
101
|
+
codex: ({ command }) => command[0] === 'exec',
|
|
102
|
+
droid: ({ command }) => command[0] === 'exec',
|
|
103
|
+
claude: printing,
|
|
104
|
+
// `atris agent spawn` launches `cursor-agent <prompt>` with no -p.
|
|
105
|
+
cursor: (split) => printing(split) || split.prompt.length > 0,
|
|
106
|
+
devin: printing,
|
|
107
|
+
grok: printing,
|
|
108
|
+
agy: printing,
|
|
109
|
+
opencode: ({ command }) => command[0] === 'run',
|
|
110
|
+
'atris-fast': ({ flags, prompt }) => !flags.some(([name]) => AX_CONTROL_FLAGS.has(name)) && prompt.length > 0,
|
|
111
|
+
commandcode: printing,
|
|
112
|
+
atris: atrisRunEngine,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Subcommand words ahead of the settings.
|
|
116
|
+
function subcommandCount(engine, args) {
|
|
117
|
+
if (engine === 'codex' || engine === 'droid' || engine === 'opencode') return 1;
|
|
118
|
+
if (engine === 'atris') return args[0] === 'engine' ? 2 : 1;
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// `atris engine ask|dispatch|validate ...` runs the engine it names;
|
|
123
|
+
// `atris engine fable <question>` asks fable; `atris chat --print` is Atris
|
|
124
|
+
// Fast. Anything else atris does is not an engine run.
|
|
125
|
+
function atrisRunEngine({ command, flags, prompt }) {
|
|
126
|
+
if (command[0] === 'chat') return printing({ flags }) && prompt.length > 0 ? 'atris-fast' : false;
|
|
127
|
+
if (command[0] !== 'engine') return false;
|
|
128
|
+
const named = canonicalEngine(flagValue(flags, ['--engine']));
|
|
129
|
+
if (ATRIS_RUN_VERBS.includes(command[1])) return named || 'atris';
|
|
130
|
+
return canonicalEngine(command[1]) || false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function canonicalEngine(name) {
|
|
134
|
+
if (!name) return '';
|
|
135
|
+
const { canonicalEngineName } = require('./engine-registry');
|
|
136
|
+
try { return canonicalEngineName(name) || ''; } catch { return ''; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The program at the front of a launch -> the engine name the roster uses.
|
|
140
|
+
const BINARY_ENGINES = Object.freeze({
|
|
141
|
+
claude: 'claude', 'cursor-agent': 'cursor', droid: 'droid', devin: 'devin', grok: 'grok', agy: 'agy', opencode: 'opencode',
|
|
142
|
+
ax: 'atris-fast', cmd: 'commandcode', atris: 'atris',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ax picks its model by mode flag; --fast is the atris-fast profile's model.
|
|
146
|
+
const AX_MODES = ['auto', 'max', 'pro', 'fast', 'rapid', 'alpha', 'code-fast'];
|
|
147
|
+
|
|
148
|
+
const INTERPRETERS = new Set(['node', 'bun', 'deno']);
|
|
149
|
+
|
|
150
|
+
function engineOf(token) {
|
|
151
|
+
const text = String(token || '');
|
|
152
|
+
const base = path.basename(text).replace(/\.(c?js|mjs)$/, '');
|
|
153
|
+
// The native codex binary can carry a platform suffix; codex-watchdog is
|
|
154
|
+
// atris's wrapper, not codex.
|
|
155
|
+
if (/^codex(-(?:aarch64|arm64|x86_64|x64)[a-z0-9_-]*)?$/.test(base)) return 'codex';
|
|
156
|
+
if (Object.prototype.hasOwnProperty.call(BINARY_ENGINES, base)) return BINARY_ENGINES[base];
|
|
157
|
+
// Command Code run straight from its package file.
|
|
158
|
+
if (/[\\/]command-code[\\/]dist[\\/]index\.m?js$/.test(text)) return 'commandcode';
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// "1-02:03:04", "02:03:04", "03:04" -> seconds.
|
|
163
|
+
function parseElapsed(text) {
|
|
164
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(String(text || '').trim());
|
|
165
|
+
if (!match) return null;
|
|
166
|
+
const [, days, hours, minutes, seconds] = match.map((part) => Number(part || 0));
|
|
167
|
+
return days * 86400 + hours * 3600 + minutes * 60 + seconds;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// "now", "6m", "2h", "3d".
|
|
171
|
+
function shortElapsed(seconds) {
|
|
172
|
+
const value = Number(seconds);
|
|
173
|
+
if (!Number.isFinite(value) || value < 0) return '-';
|
|
174
|
+
if (value < 60) return 'now';
|
|
175
|
+
if (value < 3600) return `${Math.floor(value / 60)}m`;
|
|
176
|
+
if (value < 86400) return `${Math.floor(value / 3600)}h`;
|
|
177
|
+
return `${Math.floor(value / 86400)}d`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parsePsLine(line) {
|
|
181
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(String(line || ''));
|
|
182
|
+
if (!match) return null;
|
|
183
|
+
return { pid: Number(match[1]), ppid: Number(match[2]), elapsed_seconds: parseElapsed(match[3]), command: match[4] };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The one place a launch is split. argv -> the subcommand words, the
|
|
187
|
+
// settings before the prompt, the prompt, and the settings after it.
|
|
188
|
+
// Leading settings stop at "--" or at the first word that is neither a flag
|
|
189
|
+
// nor a flag's value. Atris puts some settings after the prompt (claude
|
|
190
|
+
// --model, devin --model, fleet's sandbox flags); those count only when every
|
|
191
|
+
// word to the end is a known setting, any model named is a real model id, and
|
|
192
|
+
// there was no "--". `unsure` is set when a flag the engine list does not
|
|
193
|
+
// know came before the prompt, so its value could not be told from prose.
|
|
194
|
+
function splitArgs(engine, args) {
|
|
195
|
+
const spec = ENGINE_FLAGS[engine];
|
|
196
|
+
const command = args.slice(0, subcommandCount(engine, args));
|
|
197
|
+
const flags = [];
|
|
198
|
+
let unsure = false;
|
|
199
|
+
let i = command.length;
|
|
200
|
+
for (; i < args.length; i += 1) {
|
|
201
|
+
const arg = args[i];
|
|
202
|
+
if (arg === '--') return { command, flags, prompt: args.slice(i + 1), dashed: true, unsure, tail: [] };
|
|
203
|
+
if (!arg.startsWith('-') || arg === '-') break;
|
|
204
|
+
const eq = arg.indexOf('=');
|
|
205
|
+
if (arg.startsWith('--') && eq > 0) {
|
|
206
|
+
flags.push([arg.slice(0, eq), arg.slice(eq + 1)]);
|
|
207
|
+
if (!spec.values.has(arg.slice(0, eq)) && !spec.switches.has(arg.slice(0, eq))) unsure = true;
|
|
208
|
+
} else if (spec.values.has(arg) && i + 1 < args.length) {
|
|
209
|
+
flags.push([arg, args[i + 1]]);
|
|
210
|
+
i += 1;
|
|
211
|
+
} else {
|
|
212
|
+
flags.push([arg, true]);
|
|
213
|
+
if (!spec.switches.has(arg)) unsure = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const rest = args.slice(i);
|
|
217
|
+
const tail = trailingSettings(spec, rest);
|
|
218
|
+
return {
|
|
219
|
+
command,
|
|
220
|
+
flags,
|
|
221
|
+
prompt: tail ? rest.slice(0, tail.at) : rest,
|
|
222
|
+
dashed: false,
|
|
223
|
+
unsure,
|
|
224
|
+
tail: tail ? tail.flags : [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function looksLikeModel(value) {
|
|
229
|
+
const text = String(value || '');
|
|
230
|
+
if (/\d|\//.test(text)) return true;
|
|
231
|
+
const { isClaudeFamilyModel } = require('./engine-registry');
|
|
232
|
+
return isClaudeFamilyModel(text);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const EFFORT_VALUES = new Set([...EFFORT_WORDS, 'off', 'none', 'minimal']);
|
|
236
|
+
|
|
237
|
+
// The settings at the very end of a launch, read right to left: each step
|
|
238
|
+
// takes a known setting (with its value) off the end, and stops at the first
|
|
239
|
+
// word that is not one, at a setting already taken (a launcher never says a
|
|
240
|
+
// setting twice, so a repeat belongs to the prompt), or at a model or effort
|
|
241
|
+
// value that is not real. At least one prompt word always stays. So a prompt
|
|
242
|
+
// ending "Explain --model <some model>" keeps its words, and the launcher's
|
|
243
|
+
// own "--model <its model> ..." after it is what counts.
|
|
244
|
+
function trailingSettings(spec, words) {
|
|
245
|
+
const known = (name) => spec.values.has(name) || spec.switches.has(name);
|
|
246
|
+
const flags = [];
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
let at = words.length;
|
|
249
|
+
while (at > 1) {
|
|
250
|
+
const word = words[at - 1];
|
|
251
|
+
const before = words[at - 2];
|
|
252
|
+
let item = null;
|
|
253
|
+
let width = 1;
|
|
254
|
+
const eq = word.startsWith('--') ? word.indexOf('=') : -1;
|
|
255
|
+
if (eq > 0 && known(word.slice(0, eq))) item = [word.slice(0, eq), word.slice(eq + 1)];
|
|
256
|
+
else if (!word.startsWith('-') && spec.values.has(before) && at > 2) { item = [before, word]; width = 2; }
|
|
257
|
+
else if (spec.switches.has(word) && word !== '-p' && word !== '--print') item = [word, true];
|
|
258
|
+
if (!item || seen.has(item[0])) break;
|
|
259
|
+
if ((item[0] === '--model' || item[0] === '-m') && !looksLikeModel(item[1])) break;
|
|
260
|
+
if ((item[0] === '--effort' || item[0] === '--reasoning-effort') && !EFFORT_VALUES.has(item[1])) break;
|
|
261
|
+
seen.add(item[0]);
|
|
262
|
+
if (item[0] === '--model') seen.add('-m');
|
|
263
|
+
if (item[0] === '-m') seen.add('--model');
|
|
264
|
+
flags.unshift(item);
|
|
265
|
+
at -= width;
|
|
266
|
+
}
|
|
267
|
+
return flags.length ? { at, flags } : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hasFlag(flags, names) {
|
|
271
|
+
return flags.some(([name]) => names.includes(name));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function flagValue(flags, names) {
|
|
275
|
+
const found = flags.find(([name, value]) => names.includes(name) && typeof value === 'string');
|
|
276
|
+
return found ? found[1] : '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function effortOf(flags) {
|
|
280
|
+
const direct = flagValue(flags, ['--effort', '--reasoning-effort', '--variant']);
|
|
281
|
+
if (direct) return direct;
|
|
282
|
+
for (const [name, value] of flags) {
|
|
283
|
+
if (name !== '-c' && name !== '--config') continue;
|
|
284
|
+
const match = /^model_reasoning_effort=["']?([a-z]+)/.exec(String(value));
|
|
285
|
+
if (match) return match[1];
|
|
286
|
+
}
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function promptText(split) {
|
|
291
|
+
if (split.prompt.length) return split.prompt.join(' ');
|
|
292
|
+
const file = flagValue(split.flags, ['--prompt-file']);
|
|
293
|
+
return file ? `prompt file ${path.basename(file)}` : '';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A prompt can run to pages; the view needs its first line.
|
|
297
|
+
const DOING_MAX = 240;
|
|
298
|
+
|
|
299
|
+
// What the run is for, on one line, without the dispatch preamble
|
|
300
|
+
// ("Read <path>/MEMBER.md first.").
|
|
301
|
+
function doingText(text) {
|
|
302
|
+
const doing = String(text || '').replace(/^(?:first,?\s+)?read\s+\S+\s+first[.,]?\s*/i, '');
|
|
303
|
+
return doing.length > DOING_MAX ? `${doing.slice(0, DOING_MAX - 1).trimEnd()}…` : doing;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function axModel(engine, flags) {
|
|
307
|
+
if (engine !== 'atris-fast') return '';
|
|
308
|
+
const mode = AX_MODES.find((name) => hasFlag(flags, [`--${name}`]));
|
|
309
|
+
return mode ? `atris:${mode}` : '';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// An unsure reading: an unknown flag came before the prompt, so the words
|
|
313
|
+
// after it may be its value or the prompt. Read every flag-looking word
|
|
314
|
+
// before "--" as a setting just to decide whether this is a run, and take the
|
|
315
|
+
// prompt from after the -p when there is one.
|
|
316
|
+
function looseSplit(engine, args, split) {
|
|
317
|
+
const dash = args.indexOf('--');
|
|
318
|
+
const head = dash === -1 ? args : args.slice(0, dash);
|
|
319
|
+
const flags = head.filter((word) => word.startsWith('-')).map((word) => [word, true]);
|
|
320
|
+
const print = head.findIndex((word) => word === '-p' || word === '--print');
|
|
321
|
+
const after = print === -1 ? null : splitArgs(engine, [...split.command, ...args.slice(print + 1)]);
|
|
322
|
+
return { ...split, flags, prompt: after && after.prompt.length ? after.prompt : split.prompt };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// One ps line -> a run, or null when it is not an engine run at all.
|
|
326
|
+
function parseEngineProcess(entry) {
|
|
327
|
+
if (!entry || !entry.command) return null;
|
|
328
|
+
const tokens = entry.command.trim().split(/\s+/);
|
|
329
|
+
let at = 0;
|
|
330
|
+
if (INTERPRETERS.has(path.basename(tokens[0] || ''))) at = 1;
|
|
331
|
+
// The orchestrator daemon is skipped by what it runs, never by what a
|
|
332
|
+
// prompt mentions ("Review orchestrator.py" is real work).
|
|
333
|
+
if (tokens.slice(0, at + 1).some((token) => /orchestrator/i.test(path.basename(token)))) return null;
|
|
334
|
+
const binary = engineOf(tokens[at]);
|
|
335
|
+
if (!binary) return null;
|
|
336
|
+
const args = tokens.slice(at + 1);
|
|
337
|
+
let split = splitArgs(binary, args);
|
|
338
|
+
let verdict = RUN_RULES[binary](split);
|
|
339
|
+
if (!verdict && split.unsure) {
|
|
340
|
+
split = looseSplit(binary, args, split);
|
|
341
|
+
verdict = RUN_RULES[binary](split);
|
|
342
|
+
}
|
|
343
|
+
if (!verdict) return null;
|
|
344
|
+
const engine = typeof verdict === 'string' ? verdict : binary;
|
|
345
|
+
const prompt = promptText(split);
|
|
346
|
+
const { member, doing } = readMemberLaunch(prompt);
|
|
347
|
+
const settings = split.unsure ? [] : [...split.flags, ...split.tail];
|
|
348
|
+
const model = flagValue(settings, ['-m', '--model']) || (split.unsure ? '' : axModel(binary, split.flags));
|
|
349
|
+
return {
|
|
350
|
+
pid: entry.pid,
|
|
351
|
+
ppid: entry.ppid,
|
|
352
|
+
engine,
|
|
353
|
+
model: model || null,
|
|
354
|
+
effort: effortOf(settings) || null,
|
|
355
|
+
member,
|
|
356
|
+
doing: doingText(doing) || '-',
|
|
357
|
+
elapsed_seconds: entry.elapsed_seconds,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Every matched run above this one in the process tree. A shell may sit
|
|
362
|
+
// between a dispatcher and the tool it runs, so the walk goes all the way up.
|
|
363
|
+
function matchedAncestors(run, all, byPid) {
|
|
364
|
+
const out = [];
|
|
365
|
+
const seen = new Set();
|
|
366
|
+
let pid = run.ppid;
|
|
367
|
+
while (all.has(pid) && !seen.has(pid)) {
|
|
368
|
+
seen.add(pid);
|
|
369
|
+
if (byPid.has(pid)) out.push(byPid.get(pid));
|
|
370
|
+
pid = all.get(pid).ppid;
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Keep one run per real job. A node wrapper and the native binary it spawns
|
|
376
|
+
// carry the same words, and a dispatcher (atris engine) sits above the tool
|
|
377
|
+
// it runs: the deepest run wins, and an unnamed run takes the member of the
|
|
378
|
+
// nearest named run above it. Only runs in one process line fold together:
|
|
379
|
+
// two separate processes asking the same thing are two jobs.
|
|
380
|
+
function liveEngineRuns(entries, { selfPid = process.pid } = {}) {
|
|
381
|
+
const all = new Map(entries.filter(Boolean).map((entry) => [entry.pid, entry]));
|
|
382
|
+
const runs = entries
|
|
383
|
+
.filter((entry) => entry && entry.pid !== selfPid)
|
|
384
|
+
.map(parseEngineProcess)
|
|
385
|
+
.filter(Boolean);
|
|
386
|
+
const byPid = new Map(runs.map((run) => [run.pid, run]));
|
|
387
|
+
const above = new Map(runs.map((run) => [run.pid, matchedAncestors(run, all, byPid)]));
|
|
388
|
+
for (const run of runs) {
|
|
389
|
+
if (run.member) continue;
|
|
390
|
+
const named = above.get(run.pid).find((item) => item.member);
|
|
391
|
+
if (named) run.member = named.member;
|
|
392
|
+
}
|
|
393
|
+
const hidden = new Set();
|
|
394
|
+
for (const run of runs) {
|
|
395
|
+
for (const item of above.get(run.pid)) {
|
|
396
|
+
if (!item.member || item.member === run.member) hidden.add(item.pid);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return runs
|
|
400
|
+
.filter((run) => !hidden.has(run.pid))
|
|
401
|
+
.map(({ ppid, ...run }) => run)
|
|
402
|
+
.sort((a, b) => (a.elapsed_seconds ?? Infinity) - (b.elapsed_seconds ?? Infinity));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The live runs on this machine. deps.runPs returns { status, stdout } like
|
|
406
|
+
// spawnSync; tests pass their own. A ps that fails means no runs, never an
|
|
407
|
+
// error.
|
|
408
|
+
function listLiveEngineRuns(deps = {}) {
|
|
409
|
+
const run = deps.runPs || (() => spawnSync('ps', PS_ARGS, { encoding: 'utf8', timeout: 3000, maxBuffer: 16 * 1024 * 1024 }));
|
|
410
|
+
let result;
|
|
411
|
+
try { result = run(PS_ARGS); } catch { return []; }
|
|
412
|
+
if (!result || result.status !== 0 || typeof result.stdout !== 'string') return [];
|
|
413
|
+
const entries = result.stdout.split(/\r?\n/).map(parsePsLine).filter(Boolean);
|
|
414
|
+
return liveEngineRuns(entries, { selfPid: deps.selfPid || process.pid });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// "<engine> · <model label>" for a live run.
|
|
418
|
+
function liveRunEngineText(run) {
|
|
419
|
+
return (run && engineModelText(run.engine, run.model || '')) || '-';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = {
|
|
423
|
+
listLiveEngineRuns,
|
|
424
|
+
liveEngineRuns,
|
|
425
|
+
liveRunEngineText,
|
|
426
|
+
parseEngineProcess,
|
|
427
|
+
parsePsLine,
|
|
428
|
+
splitArgs,
|
|
429
|
+
shortElapsed,
|
|
430
|
+
};
|
package/lib/engine-registry.js
CHANGED
|
@@ -923,16 +923,25 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
923
923
|
let namedModel = false;
|
|
924
924
|
let namedEffort = false;
|
|
925
925
|
let prep = '';
|
|
926
|
+
let verified = '';
|
|
926
927
|
for (const raw of extras) {
|
|
927
|
-
// "max: 20 min"
|
|
928
|
-
|
|
928
|
+
// "max: 20 min", "until: 2026-10-24", and "verified: 2026-09-30" read
|
|
929
|
+
// like "max 20 min".
|
|
930
|
+
const part = raw.replace(/^(max|until|verified)\s*:\s*/i, '$1 ');
|
|
929
931
|
const backupMatch = /^backup\s+(.+)$/i.exec(part);
|
|
930
932
|
const untilMatch = /^until\s+(.+)$/i.exec(part);
|
|
931
933
|
const maxMatch = /^max\b/i.test(part) && !/^max$/i.test(part);
|
|
932
934
|
const modelMatch = /^model\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
933
935
|
const effortMatch = /^effort\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
934
936
|
const prepMatch = /^prep\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
935
|
-
|
|
937
|
+
const verifiedMatch = /^verified\s+(.+)$/i.exec(part);
|
|
938
|
+
if (verifiedMatch && !verified) {
|
|
939
|
+
// "verified 2026-09-30": the day a person last saw this model answer.
|
|
940
|
+
// The roster flags a pin whose newest proof is over 30 days old.
|
|
941
|
+
const words = verifiedMatch[1].trim();
|
|
942
|
+
verified = parseRosterUntilWords(words);
|
|
943
|
+
if (!verified) throw new Error(`"verified ${words}" is not a date; use verified 2026-09-30`);
|
|
944
|
+
} else if (prepMatch && !prep) {
|
|
936
945
|
// "prep: search": that job's lead reads the task first and hands this
|
|
937
946
|
// worker a trimmed brief. Whether it names this same job is checked
|
|
938
947
|
// where the job is known.
|
|
@@ -988,6 +997,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
988
997
|
until,
|
|
989
998
|
...(until ? {} : { never_expires: true }),
|
|
990
999
|
...(untilYearless ? { until_needs_year: untilYearless } : {}),
|
|
1000
|
+
...(verified ? { verified } : {}),
|
|
991
1001
|
};
|
|
992
1002
|
}
|
|
993
1003
|
|
|
@@ -1030,6 +1040,7 @@ function rosterWorkerFromValue(value, role, options = {}) {
|
|
|
1030
1040
|
until: pick.until,
|
|
1031
1041
|
...(pick.never_expires ? { never_expires: true } : {}),
|
|
1032
1042
|
...(pick.until_needs_year ? { until_needs_year: pick.until_needs_year } : {}),
|
|
1043
|
+
...(pick.verified ? { verified: pick.verified } : {}),
|
|
1033
1044
|
};
|
|
1034
1045
|
}
|
|
1035
1046
|
|
|
@@ -1078,6 +1089,7 @@ function pickWorkers(pick) {
|
|
|
1078
1089
|
...(pick.prep ? { prep: pick.prep } : {}),
|
|
1079
1090
|
until: pick.until,
|
|
1080
1091
|
...(pick.never_expires ? { never_expires: true } : {}),
|
|
1092
|
+
...(pick.verified ? { verified: pick.verified } : {}),
|
|
1081
1093
|
...(pick.line ? { line: pick.line } : {}),
|
|
1082
1094
|
}];
|
|
1083
1095
|
if (pick.backup) {
|
|
@@ -1383,6 +1395,7 @@ function workerLineText(worker) {
|
|
|
1383
1395
|
if (Number(worker.max_seconds) > 0) parts.push(`max: ${rosterMaxText(worker.max_seconds).replace(/^max /, '')}`);
|
|
1384
1396
|
if (worker.prep) parts.push(`prep: ${rosterJobLabel(worker.prep)}`);
|
|
1385
1397
|
if (worker.until) parts.push(`until ${worker.until}`);
|
|
1398
|
+
if (worker.verified) parts.push(`verified ${worker.verified}`);
|
|
1386
1399
|
return `- ${parts.join(', ')}`;
|
|
1387
1400
|
}
|
|
1388
1401
|
|
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', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'buy', 'transactions', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', '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', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// How a launch names the member it runs as, in one place. The writers use
|
|
4
|
+
// these lines and the team view reads them back with patterns built from the
|
|
5
|
+
// same lines, so the wording cannot drift apart.
|
|
6
|
+
//
|
|
7
|
+
// Mission ticks (atris member run, atris mission) say:
|
|
8
|
+
// You are the member "validator".
|
|
9
|
+
// **Objective:** <what the run is for>
|
|
10
|
+
// Dispatch briefs say:
|
|
11
|
+
// You are acting as validator. <the work>
|
|
12
|
+
|
|
13
|
+
const escapeRegExp = require('./escape-regexp');
|
|
14
|
+
|
|
15
|
+
const NAME = '([a-z0-9][a-z0-9_-]*[a-z0-9])';
|
|
16
|
+
const SLOT = '\u0000';
|
|
17
|
+
|
|
18
|
+
function memberIdentityLine(name) {
|
|
19
|
+
return `You are the member "${name}".`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function missionObjectiveLine(objective) {
|
|
23
|
+
return `**Objective:** ${objective}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function patternFrom(line, name) {
|
|
27
|
+
return new RegExp(escapeRegExp(line).replace(escapeRegExp(SLOT), name), 'i');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MISSION_MEMBER = patternFrom(memberIdentityLine(SLOT), NAME);
|
|
31
|
+
const DISPATCH_MEMBER = /\bacting as (?:the )?([a-z0-9][a-z0-9_-]*[a-z0-9])[.,:;]?/i;
|
|
32
|
+
const OBJECTIVE = patternFrom(missionObjectiveLine(SLOT), '(.+?)(?=\\s\\*\\*[A-Z][^*]*:\\*\\*|\\s#|$)');
|
|
33
|
+
|
|
34
|
+
// One prompt -> { member, doing }. The member is the name the prompt runs
|
|
35
|
+
// as, or null. doing is what the run is for: a mission's objective, or the
|
|
36
|
+
// words after the dispatch line.
|
|
37
|
+
function readMemberLaunch(prompt) {
|
|
38
|
+
const text = String(prompt || '').replace(/\\0?12|\\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
39
|
+
const mission = MISSION_MEMBER.exec(text);
|
|
40
|
+
if (mission) {
|
|
41
|
+
const objective = OBJECTIVE.exec(text);
|
|
42
|
+
return { member: mission[1].toLowerCase(), doing: objective ? objective[1].trim() : text };
|
|
43
|
+
}
|
|
44
|
+
const dispatch = DISPATCH_MEMBER.exec(text);
|
|
45
|
+
if (dispatch) {
|
|
46
|
+
const after = text.slice(dispatch.index + dispatch[0].length).trim();
|
|
47
|
+
return { member: dispatch[1].toLowerCase(), doing: after || text };
|
|
48
|
+
}
|
|
49
|
+
return { member: null, doing: text };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
memberIdentityLine,
|
|
54
|
+
missionObjectiveLine,
|
|
55
|
+
readMemberLaunch,
|
|
56
|
+
};
|
package/lib/roster-models.js
CHANGED
|
@@ -97,6 +97,16 @@ function engineRunsView(engineId, { model = '', effort = '' } = {}, options = {}
|
|
|
97
97
|
return view;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// "<engine> · <model label>" for a table cell: the tool, a middle dot, and the
|
|
101
|
+
// model label, or "own default" when the tool picks its model itself.
|
|
102
|
+
// Effort is left out; tables give it its own line.
|
|
103
|
+
function engineModelText(engine, model = '') {
|
|
104
|
+
const id = String(engine || '').trim();
|
|
105
|
+
if (!id) return '';
|
|
106
|
+
const label = model ? modelLabel(model) : '';
|
|
107
|
+
return `${id} · ${label || 'own default'}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
function runsDetail(view) {
|
|
101
111
|
const items = [{ text: view.model ? modelLabel(view.model) : 'its own default', source: view.model ? view.model_source : 'roster' }];
|
|
102
112
|
if (view.effort) items.push({ text: view.effort, source: view.effort_source });
|
|
@@ -172,6 +182,7 @@ function availableModels(engines, options = {}) {
|
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
module.exports = {
|
|
185
|
+
engineModelText,
|
|
175
186
|
modelLabel,
|
|
176
187
|
readCodexSettings,
|
|
177
188
|
engineRunsView,
|