atris 3.82.18 → 3.82.20
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 +17 -2
- package/ax +6 -2
- package/bin/atris.js +5 -0
- package/commands/ask.js +202 -13
- package/commands/engine.js +49 -7
- package/commands/film.js +191 -0
- package/commands/mission.js +11 -1
- package/commands/probe.js +108 -11
- package/lib/engine-ask.js +10 -1
- package/lib/engine-processes.js +7 -2
- package/lib/engine-registry.js +58 -2
- package/lib/fleet.js +1 -1
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +66 -9
- package/lib/mission-core.js +7 -0
- package/lib/model-names.js +13 -0
- package/lib/runner-command.js +10 -0
- package/lib/runs-on.js +134 -0
- package/package.json +1 -1
package/commands/mission.js
CHANGED
|
@@ -8217,6 +8217,9 @@ function detectUnavailableModel(text) {
|
|
|
8217
8217
|
// need a resume; a model-unavailable pause is a config error a bare resume won't fix,
|
|
8218
8218
|
// so name the dead id and the two knobs that change it.
|
|
8219
8219
|
function missionPauseNextAction(pauseReason, missionId, deadModel = null, lastErrorReason = null) {
|
|
8220
|
+
if (pauseReason === 'member-runs-elsewhere') {
|
|
8221
|
+
return 'left for the machine its owner works on; to run it here, move the owner back: atris engine roster set --member <name> --on local';
|
|
8222
|
+
}
|
|
8220
8223
|
if (pauseReason === 'protected-lane-review') {
|
|
8221
8224
|
return `review the protected diff and its receipt before resuming: atris mission run ${missionId}`;
|
|
8222
8225
|
}
|
|
@@ -9577,6 +9580,11 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9577
9580
|
let cachedStep = findCachedMissionStepReceipt(cwd, { missionId: mission.id, tickIndex: tickIdx });
|
|
9578
9581
|
let cachedVerifierResult = cachedStep ? cachedStep.verifier_result : null;
|
|
9579
9582
|
const tickSelection = resolveMissionTickRunner(runtimeMission, cwd);
|
|
9583
|
+
if (tickSelection.runs_elsewhere) {
|
|
9584
|
+
console.error(`${mission.owner} runs on ${tickSelection.runs_elsewhere}, so this machine leaves the work for it.`);
|
|
9585
|
+
pauseReason = 'member-runs-elsewhere';
|
|
9586
|
+
break;
|
|
9587
|
+
}
|
|
9580
9588
|
const tickRuntimeMission = tickSelection.mission;
|
|
9581
9589
|
const tickRunnerName = String(tickRuntimeMission.runner || '').trim().toLowerCase();
|
|
9582
9590
|
const tickCallerSessionRunner = runnerUsesCallerSession(tickRunnerName);
|
|
@@ -10406,7 +10414,9 @@ function completeMissionRunPhase(context) {
|
|
|
10406
10414
|
// 'no-progress' is a clean stop the run diagnosed itself, never route it
|
|
10407
10415
|
// through handleMissionBlocker (that would file a blocker task and
|
|
10408
10416
|
// dispatch an engine to "fix" a mission that correctly stopped itself).
|
|
10409
|
-
|
|
10417
|
+
// 'member-runs-elsewhere' is the same: the owner's own machine does the
|
|
10418
|
+
// work, so nothing here is broken and nothing local should "fix" it.
|
|
10419
|
+
const blockerReason = ['no-progress', 'budget-exhausted', 'member-runs-elsewhere'].includes(pauseReason)
|
|
10410
10420
|
? null
|
|
10411
10421
|
: (pauseReason || mission.stop_reason || (mission.status === 'blocked' ? 'verifier-failed' : null));
|
|
10412
10422
|
if (selfDrive && blockerReason) {
|
package/commands/probe.js
CHANGED
|
@@ -59,14 +59,58 @@ function normalizeBash(cmd, label) {
|
|
|
59
59
|
return cmd.split(l + '/').join('').split(l).join('.');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
62
|
+
// Every file op the server's local_file_op offers: the enum of LOCAL_FILE_TOOL
|
|
63
|
+
// in the backend (backend/rl/tool_policy_bench.py). The backend test
|
|
64
|
+
// test_local_file_op_probe_lockstep.py pins that enum to this list, so the
|
|
65
|
+
// server cannot add an op without that test failing until this table and
|
|
66
|
+
// SERVER_FILE_OPS are updated together. test/probe-file-ops.test.js fails if
|
|
67
|
+
// any op listed here has no command below.
|
|
68
|
+
const SERVER_FILE_OPS = ['list', 'search', 'glob', 'inspect', 'read', 'write', 'append', 'edit', 'bash'];
|
|
69
|
+
|
|
70
|
+
// Directory names the server hides from glob unless include_generated is set
|
|
71
|
+
// (IGNORED_WORKSPACE_PARTS in the same backend file).
|
|
72
|
+
const IGNORED_PARTS = ['.git', '.next', '.pytest_cache', '.venv', '__pycache__', 'build', 'dist', 'node_modules', 'release', 'venv'];
|
|
73
|
+
|
|
74
|
+
// The `..` guard. It covers args.path and, for the ops that take more than one
|
|
75
|
+
// path, every `paths` entry and the glob pattern. The `bash` op still executes
|
|
76
|
+
// whatever command the model sends, verbatim, on the remote ai-computer, that
|
|
77
|
+
// is the production relay contract and the table must stay in lockstep with
|
|
78
|
+
// it, so this probe is NOT read-only.
|
|
79
|
+
function hasParentRef(value) {
|
|
80
|
+
return String(value).split('/').includes('..');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function boundedInt(value, fallback, low, high) {
|
|
84
|
+
const parsed = Number.parseInt(value, 10);
|
|
85
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
86
|
+
return Math.min(Math.max(parsed, low), high);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function b64(value) {
|
|
90
|
+
return Buffer.from(String(value), 'utf8').toString('base64');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Same edit the server runs (_python_edit_command): replace the first match.
|
|
94
|
+
const PY_EDIT = 'import base64,pathlib,sys;'
|
|
95
|
+
+ 'p=pathlib.Path(sys.argv[1]);text=p.read_text(encoding=\'utf-8\');'
|
|
96
|
+
+ 'old=base64.b64decode(sys.argv[2]).decode(\'utf-8\');'
|
|
97
|
+
+ 'new=base64.b64decode(sys.argv[3]).decode(\'utf-8\');'
|
|
98
|
+
+ 'old in text or sys.exit(\'find text not found\');'
|
|
99
|
+
+ 'p.write_text(text.replace(old,new,1),encoding=\'utf-8\')';
|
|
100
|
+
|
|
101
|
+
// glob like the server's pathlib glob: `**` recurses, hidden files match,
|
|
102
|
+
// directories end in `/`, generated directories are hidden unless asked for.
|
|
103
|
+
const PY_GLOB = 'import glob,os,pathlib,sys\n'
|
|
104
|
+
+ 'pat,limit,gen,ign=sys.argv[1],int(sys.argv[2]),sys.argv[3]==\'1\',sys.argv[4].split(\',\')\n'
|
|
105
|
+
+ 'raw=map(str,pathlib.Path(\'.\').glob(pat)) if not os.path.isabs(pat) else glob.glob(pat,recursive=True)\n'
|
|
106
|
+
+ 'hits=sorted({h+(\'/\' if os.path.isdir(h) else \'\') for h in raw if gen or not any(x in ign for x in h.split(\'/\'))})\n'
|
|
107
|
+
+ 'print(\'\\n\'.join(hits[:limit]))\n'
|
|
108
|
+
+ 'len(hits)>limit and print(\'[%d more match(es) not shown]\'%(len(hits)-limit))';
|
|
109
|
+
|
|
66
110
|
function fileOpCommand(args, label) {
|
|
67
111
|
const op = String(args.type || '').toLowerCase();
|
|
68
112
|
const raw = normalizePath(String(args.path || '') || '.', label);
|
|
69
|
-
if (raw
|
|
113
|
+
if (hasParentRef(raw)) return null;
|
|
70
114
|
const p = shq(raw);
|
|
71
115
|
if (op === 'bash') return `( ${normalizeBash(String(args.command || '') || 'true', label)} )`;
|
|
72
116
|
if (op === 'list') return `find ${p} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' | head -200`;
|
|
@@ -74,7 +118,42 @@ function fileOpCommand(args, label) {
|
|
|
74
118
|
const q = shq(String(args.query || args.pattern || ''));
|
|
75
119
|
return `grep -rn -m 50 ${q} ${p} 2>/dev/null | head -50`;
|
|
76
120
|
}
|
|
121
|
+
// The server's read with a wildcard (*, ?, [) is a glob.
|
|
122
|
+
if (op === 'read' && /[*?[]/.test(raw)) return fileOpCommand({ ...args, type: 'glob', pattern: raw }, label);
|
|
77
123
|
if (op === 'read') return `{ [ -d ${p} ] && ls -p ${p} | head -200 || head -c 12000 ${p}; }`;
|
|
124
|
+
if (op === 'glob') {
|
|
125
|
+
const pattern = normalizePath(String(args.pattern || args.query || args.path || '').trim(), label);
|
|
126
|
+
if (!pattern || hasParentRef(pattern)) return null;
|
|
127
|
+
const limit = boundedInt(args.max_results, 200, 1, 1000);
|
|
128
|
+
return `python3 -c ${shq(PY_GLOB)} ${shq(pattern)} ${limit} ${args.include_generated ? 1 : 0} ${shq(IGNORED_PARTS.join(','))}`;
|
|
129
|
+
}
|
|
130
|
+
if (op === 'inspect') {
|
|
131
|
+
const depth = boundedInt(args.max_depth, 2, 1, 4);
|
|
132
|
+
const limit = boundedInt(args.max_results, 80, 1, 300);
|
|
133
|
+
const rawPaths = args.paths;
|
|
134
|
+
const listed = Array.isArray(rawPaths)
|
|
135
|
+
? rawPaths.map((item) => String(item).trim()).filter(Boolean)
|
|
136
|
+
: String(rawPaths || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
137
|
+
const reads = listed.slice(0, 8).map((item) => normalizePath(item, label));
|
|
138
|
+
if (reads.some(hasParentRef)) return null;
|
|
139
|
+
const pieces = [`find ${p} -mindepth 1 -maxdepth ${depth} -print | head -${limit}`];
|
|
140
|
+
for (const rel of reads) pieces.push(`printf '\\n== %s ==\\n' ${shq(rel)}; head -c 12000 ${shq(rel)}`);
|
|
141
|
+
const verify = String(args.command || '').trim();
|
|
142
|
+
if (verify) pieces.push(`( ${normalizeBash(verify, label)} )`);
|
|
143
|
+
return pieces.join('; ');
|
|
144
|
+
}
|
|
145
|
+
if (op === 'write' || op === 'append') {
|
|
146
|
+
// A write needs a real file path and its content. The server writes an
|
|
147
|
+
// empty file when content is missing; a probe call like that is malformed.
|
|
148
|
+
if (!String(args.path || '').trim() || raw === '.' || typeof args.content !== 'string') return null;
|
|
149
|
+
const parent = shq(path.posix.dirname(raw));
|
|
150
|
+
return `mkdir -p -- ${parent} && printf %s ${shq(b64(args.content))} | base64 -d ${op === 'append' ? '>>' : '>'} ${p}`;
|
|
151
|
+
}
|
|
152
|
+
if (op === 'edit') {
|
|
153
|
+
const find = String(args.find || '');
|
|
154
|
+
if (!String(args.path || '').trim() || raw === '.' || !find) return null;
|
|
155
|
+
return `python3 -c ${shq(PY_EDIT)} ${p} ${b64(find)} ${b64(args.replace || '')}`;
|
|
156
|
+
}
|
|
78
157
|
return null;
|
|
79
158
|
}
|
|
80
159
|
|
|
@@ -95,6 +174,16 @@ const SIMPLE_CLI_OPS = {
|
|
|
95
174
|
mission_status: ['mission', 'status', '--json'],
|
|
96
175
|
};
|
|
97
176
|
|
|
177
|
+
// Things the server relays or offers that this harness does not run. A model
|
|
178
|
+
// that uses one gets an error back and the probe reports "probe limit", which
|
|
179
|
+
// says the harness could not vouch for that call, not that the product broke.
|
|
180
|
+
// Anything the server never offers (an invented op or tool) stays a failure.
|
|
181
|
+
const NOT_RUN_CLI_OPS = new Set([
|
|
182
|
+
'apps_list', 'atris_search', 'imessage_doctor', 'imessage_latest',
|
|
183
|
+
'task_create', 'task_update', 'skill_audit', 'codex_goal_status',
|
|
184
|
+
]);
|
|
185
|
+
const NOT_RUN_TOOLS = new Set(['dispatch_scout']);
|
|
186
|
+
|
|
98
187
|
function atrisCliCommand(args) {
|
|
99
188
|
const op = String(args.type || '').toLowerCase();
|
|
100
189
|
let argv = SIMPLE_CLI_OPS[op] || null;
|
|
@@ -269,6 +358,7 @@ async function runAtris2Turn(opts = {}) {
|
|
|
269
358
|
let engine = null;
|
|
270
359
|
const cliOps = [];
|
|
271
360
|
const unsupported = [];
|
|
361
|
+
const limits = [];
|
|
272
362
|
|
|
273
363
|
try {
|
|
274
364
|
await new Promise((resolve, reject) => {
|
|
@@ -324,8 +414,8 @@ async function runAtris2Turn(opts = {}) {
|
|
|
324
414
|
if (name === 'local_file_op') {
|
|
325
415
|
const cmd = fileOpCommand(args, label);
|
|
326
416
|
if (cmd === null) {
|
|
327
|
-
out = { status: 'error', error: '
|
|
328
|
-
unsupported.push(`file_op:${op}`);
|
|
417
|
+
out = { status: 'error', error: SERVER_FILE_OPS.includes(op) ? 'refused: unsafe path or missing argument' : 'unsupported op' };
|
|
418
|
+
unsupported.push(SERVER_FILE_OPS.includes(op) ? `file_op_refused:${op}` : `file_op:${op}`);
|
|
329
419
|
} else {
|
|
330
420
|
const term = localCwd
|
|
331
421
|
? await runLocalTerminal(cmd, localCwd, localEnv || process.env, localPathPrefix)
|
|
@@ -344,7 +434,7 @@ async function runAtris2Turn(opts = {}) {
|
|
|
344
434
|
const cmd = atrisCliCommand(args);
|
|
345
435
|
if (cmd === null) {
|
|
346
436
|
out = { status: 'error', error: `unsupported atris cli op: ${op || '?'}` };
|
|
347
|
-
unsupported.push(`cli_op:${op || '?'}`);
|
|
437
|
+
(NOT_RUN_CLI_OPS.has(op) ? limits : unsupported).push(`cli_op:${op || '?'}`);
|
|
348
438
|
} else {
|
|
349
439
|
const term = localCwd
|
|
350
440
|
? await runLocalTerminal(cmd, localCwd, localEnv || process.env, localPathPrefix)
|
|
@@ -354,7 +444,7 @@ async function runAtris2Turn(opts = {}) {
|
|
|
354
444
|
}
|
|
355
445
|
} else {
|
|
356
446
|
out = { status: 'error', error: `unsupported relayed tool: ${name || '?'}` };
|
|
357
|
-
unsupported.push(`tool:${name || '?'}`);
|
|
447
|
+
(NOT_RUN_TOOLS.has(name) ? limits : unsupported).push(`tool:${name || '?'}`);
|
|
358
448
|
}
|
|
359
449
|
await postJson(`${base}/atris2/turn/tool-result`, token,
|
|
360
450
|
buildToolResultBody(ev.call_id || ev.id, out), 30000);
|
|
@@ -399,6 +489,8 @@ async function runAtris2Turn(opts = {}) {
|
|
|
399
489
|
out.tools_run = toolsRun;
|
|
400
490
|
out.cli_ops = cliOps;
|
|
401
491
|
out.unsupported = unsupported;
|
|
492
|
+
// Only present when the harness skipped something, so the result keeps its old shape otherwise.
|
|
493
|
+
if (limits.length) out.limits = limits;
|
|
402
494
|
out.error = err;
|
|
403
495
|
out.ok = err === null;
|
|
404
496
|
out.duration_ms = Date.now() - t0;
|
|
@@ -429,6 +521,7 @@ async function probeCommand(argv) {
|
|
|
429
521
|
const err = turn.error;
|
|
430
522
|
const engine = turn.engine;
|
|
431
523
|
const unsupported = turn.unsupported;
|
|
524
|
+
const limits = turn.limits || [];
|
|
432
525
|
|
|
433
526
|
// Name the dead-end (first match wins); null = converged.
|
|
434
527
|
let deadEnd = null;
|
|
@@ -451,9 +544,12 @@ async function probeCommand(argv) {
|
|
|
451
544
|
const secs = Math.round((Date.now() - t0) / 100) / 10;
|
|
452
545
|
const ok = deadEnd === null;
|
|
453
546
|
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
454
|
-
const
|
|
547
|
+
const limitNote = limits.length
|
|
548
|
+
? ` · probe limit (this harness did not run it, not a product failure): ${limits.slice(0, 3).join(',')}`
|
|
549
|
+
: '';
|
|
550
|
+
const detail = (ok
|
|
455
551
|
? `answer: ${resultText.slice(0, 80).replace(/\n/g, ' ')}`
|
|
456
|
-
: `dead-end: ${deadEnd}
|
|
552
|
+
: `dead-end: ${deadEnd}`) + limitNote;
|
|
457
553
|
const line = buildProbeLine({ stamp, where, model: a.model, member, engine, ok, secs, toolsRun, calendar: a.calendar, cliCalendarOps, detail });
|
|
458
554
|
console.log(line);
|
|
459
555
|
return ok ? 0 : 1;
|
|
@@ -477,6 +573,7 @@ module.exports = {
|
|
|
477
573
|
normalizePath,
|
|
478
574
|
normalizeBash,
|
|
479
575
|
fileOpCommand,
|
|
576
|
+
SERVER_FILE_OPS,
|
|
480
577
|
atrisCliCommand,
|
|
481
578
|
atrisCliResult,
|
|
482
579
|
runLocalTerminal,
|
package/lib/engine-ask.js
CHANGED
|
@@ -34,7 +34,7 @@ const MAX_ASK_PROMPT_BYTES = 16 * 1024;
|
|
|
34
34
|
const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
|
|
35
35
|
const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
|
|
36
36
|
const ASK_STOP_GRACE_MS = 250;
|
|
37
|
-
const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy', 'opencode', 'commandcode']);
|
|
37
|
+
const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'muse', 'agy', 'opencode', 'commandcode']);
|
|
38
38
|
const READ_ONLY_PREAMBLE = [
|
|
39
39
|
'This is a read-only request.',
|
|
40
40
|
'Do not modify files, create worktrees, start background agents, or run commands with side effects.',
|
|
@@ -324,6 +324,15 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
|
|
|
324
324
|
args: ['--no-memory', '--no-subagents', '--permission-mode', 'plan', '--sandbox', 'read-only', ...(model ? ['--model', model] : []), ...effortArgs, '-p', request],
|
|
325
325
|
};
|
|
326
326
|
}
|
|
327
|
+
if (engine === 'muse') {
|
|
328
|
+
// --disable-write blocks file edits; the read-only preamble covers the
|
|
329
|
+
// shell. Network stays on so a question can read atris memory.
|
|
330
|
+
return {
|
|
331
|
+
engine,
|
|
332
|
+
bin: profile.bin,
|
|
333
|
+
args: ['exec', '--sandbox-network', 'enabled', '--disable-write', ...(model ? ['--model', model] : []), request],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
327
336
|
if (engine === 'agy') {
|
|
328
337
|
// agy prompts for tool permission even in plan mode and a headless ask
|
|
329
338
|
// has no one to answer, so it denies itself `ls` and fails every ask
|
package/lib/engine-processes.js
CHANGED
|
@@ -54,6 +54,10 @@ const ENGINE_FLAGS = {
|
|
|
54
54
|
values: ['--permission-mode', '--sandbox', '--reasoning-effort', '--best-of-n'],
|
|
55
55
|
switches: ['--always-approve', '--no-memory', '--no-subagents'],
|
|
56
56
|
},
|
|
57
|
+
muse: {
|
|
58
|
+
values: ['--sandbox-network', '--model', '--approval-mode', '--permission-profile'],
|
|
59
|
+
switches: ['--trust-workspace', '--disable-write', '--disable-approval', '--disable-sandbox', '--yolo'],
|
|
60
|
+
},
|
|
57
61
|
agy: {
|
|
58
62
|
values: ['--mode', '--print-timeout'],
|
|
59
63
|
switches: ['--sandbox', '--dangerously-skip-permissions'],
|
|
@@ -105,6 +109,7 @@ const RUN_RULES = {
|
|
|
105
109
|
cursor: (split) => printing(split) || split.prompt.length > 0,
|
|
106
110
|
devin: printing,
|
|
107
111
|
grok: printing,
|
|
112
|
+
muse: ({ command }) => command[0] === 'exec',
|
|
108
113
|
agy: printing,
|
|
109
114
|
opencode: ({ command }) => command[0] === 'run',
|
|
110
115
|
'atris-fast': ({ flags, prompt }) => !flags.some(([name]) => AX_CONTROL_FLAGS.has(name)) && prompt.length > 0,
|
|
@@ -114,7 +119,7 @@ const RUN_RULES = {
|
|
|
114
119
|
|
|
115
120
|
// Subcommand words ahead of the settings.
|
|
116
121
|
function subcommandCount(engine, args) {
|
|
117
|
-
if (engine === 'codex' || engine === 'droid' || engine === 'opencode') return 1;
|
|
122
|
+
if (engine === 'codex' || engine === 'droid' || engine === 'opencode' || engine === 'muse') return 1;
|
|
118
123
|
if (engine === 'atris') return args[0] === 'engine' ? 2 : 1;
|
|
119
124
|
return 0;
|
|
120
125
|
}
|
|
@@ -138,7 +143,7 @@ function canonicalEngine(name) {
|
|
|
138
143
|
|
|
139
144
|
// The program at the front of a launch -> the engine name the roster uses.
|
|
140
145
|
const BINARY_ENGINES = Object.freeze({
|
|
141
|
-
claude: 'claude', 'cursor-agent': 'cursor', droid: 'droid', devin: 'devin', grok: 'grok', agy: 'agy', opencode: 'opencode',
|
|
146
|
+
claude: 'claude', 'cursor-agent': 'cursor', droid: 'droid', devin: 'devin', grok: 'grok', muse: 'muse', agy: 'agy', opencode: 'opencode',
|
|
142
147
|
ax: 'atris-fast', cmd: 'commandcode', atris: 'atris',
|
|
143
148
|
});
|
|
144
149
|
|
package/lib/engine-registry.js
CHANGED
|
@@ -174,6 +174,10 @@ const ENGINE_SEED_META = Object.freeze({
|
|
|
174
174
|
// Devin gathers context for search only when a roster picks it.
|
|
175
175
|
devin: Object.freeze({ tier: 'max', roles: Object.freeze(['executor']), roster_only_roles: Object.freeze(['navigator']), models: Object.freeze(['built-in router']), duty: 'errands', fallback_order: 40 }),
|
|
176
176
|
grok: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['grok 4.7 fast', 'grok 4.7']), fallback_order: 45 }),
|
|
177
|
+
// Muse runs only Meta's Muse models. The router hands it build work last,
|
|
178
|
+
// after every engine with a longer record here; a roster pick may also give
|
|
179
|
+
// it search or review, so a member seat can sit on Muse whatever its job.
|
|
180
|
+
muse: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), roster_only_roles: Object.freeze(['navigator', 'validator']), models: Object.freeze(['muse-wonder-1.0', 'muse-spark-1.3']), fallback_order: 97 }),
|
|
177
181
|
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5.5', 'opus 5', 'fable', 'haiku']), duty: 'leader', fallback_order: 50 }),
|
|
178
182
|
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), models: Object.freeze(['composer 2.5']), fallback_order: 60, hosted: true }),
|
|
179
183
|
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator', 'navigator']), role_order: Object.freeze({ navigator: 5 }), models: Object.freeze(['haiku']), fallback_order: 70 }),
|
|
@@ -865,6 +869,7 @@ function resolveEngineModelWords(text) {
|
|
|
865
869
|
}
|
|
866
870
|
if (/^(opus|sonnet|haiku|fable|opusplan)\b/.test(lower)) return { engine: 'claude', model: model('claude', clean) };
|
|
867
871
|
if (/^grok/.test(lower)) return { engine: 'grok', model: model('grok', clean) };
|
|
872
|
+
if (/^muse-/.test(lower)) return { engine: 'muse', model: model('muse', clean) };
|
|
868
873
|
if (/^swe-/.test(lower)) return { engine: 'devin', model: clean };
|
|
869
874
|
if (/^gemini-/.test(lower)) return { engine: 'agy', model: clean };
|
|
870
875
|
throw new Error(`"${clean}" is not an engine or a model atris knows`);
|
|
@@ -933,7 +938,10 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
933
938
|
const effortMatch = /^effort\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
934
939
|
const prepMatch = /^prep\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
935
940
|
const verifiedMatch = /^verified\s+(.+)$/i.exec(part);
|
|
936
|
-
if (
|
|
941
|
+
if (/^on\s*:/i.test(part)) {
|
|
942
|
+
// "on: atris computer": where the member works. Read with the team
|
|
943
|
+
// line in member-engine.js; it does not change the engine pick.
|
|
944
|
+
} else if (verifiedMatch && !verified) {
|
|
937
945
|
// "verified 2026-09-30": the day a person last saw this model answer.
|
|
938
946
|
// The roster flags a pin whose newest proof is over 30 days old.
|
|
939
947
|
const words = verifiedMatch[1].trim();
|
|
@@ -1816,14 +1824,60 @@ function setTeamPick(memberName, engineName, options = {}, root = process.cwd())
|
|
|
1816
1824
|
const base = existing !== null ? sectionedRosterText(existing, file, { ...options, root }) : rosterMarkdownFromPicks(layer.picks);
|
|
1817
1825
|
// a swap changes only the lead of the member's line already in this file
|
|
1818
1826
|
const line = scanRosterMarkdown(base).entries.find((entry) => entry.section === 'team' && !entry.malformed && entry.name.trim().toLowerCase() === member);
|
|
1819
|
-
|
|
1827
|
+
// the member's place ("on: grok bot") stays when its engine changes
|
|
1828
|
+
const { splitRunsOn } = require('./runs-on');
|
|
1829
|
+
const kept = line ? splitRunsOn(line.value) : { rest: '', on: '' };
|
|
1830
|
+
// read the line without its place, wherever the on: part sits
|
|
1831
|
+
let value = (kept.rest && teamValueWithLead(kept.rest, engineId, model, role, { root }))
|
|
1820
1832
|
|| workerLineText({ engine: engineId, model }).replace(/^-\s+/, '');
|
|
1833
|
+
if (kept.on && !splitRunsOn(value).on) value = `${value}, on: ${kept.on}`;
|
|
1821
1834
|
rosterPickFromValue(value, role, { root });
|
|
1822
1835
|
writeAtomic(file, replaceTeamLine(base, member, value));
|
|
1823
1836
|
if (target.scope === 'session') sweepStaleSessionRosters(options);
|
|
1824
1837
|
return { member, engine: engineId, model, value, scope: target.scope, file };
|
|
1825
1838
|
}
|
|
1826
1839
|
|
|
1840
|
+
// Throws the plain sentence setTeamPlace would, before anything is written.
|
|
1841
|
+
function setTeamPlaceCheck(placeWords) {
|
|
1842
|
+
const { readRunsOn } = require('./runs-on');
|
|
1843
|
+
return readRunsOn(placeWords, { isEngine: (words) => canonicalEngineName(words) });
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Say where one member works: "atris computer", "local", or a bot on another
|
|
1847
|
+
// machine ("grok bot"). The rest of the member's line stays as written. Local
|
|
1848
|
+
// drops the on: part, since local is what a line with no place means.
|
|
1849
|
+
function setTeamPlace(memberName, placeWords, options = {}, root = process.cwd()) {
|
|
1850
|
+
const { replaceTeamLine, scanRosterMarkdown } = require('./roster-markdown');
|
|
1851
|
+
const { readRunsOn, splitRunsOn } = require('./runs-on');
|
|
1852
|
+
const member = String(memberName || '').trim().toLowerCase();
|
|
1853
|
+
if (!/^[a-z0-9._-]+$/.test(member)) throw new Error(`"${memberName}" is not a member name`);
|
|
1854
|
+
const place = readRunsOn(placeWords, { isEngine: (words) => canonicalEngineName(words) });
|
|
1855
|
+
const target = rosterWriteTarget(options, root);
|
|
1856
|
+
const registry = readEngineRegistry(root, { persist: false });
|
|
1857
|
+
const layer = target.layer || readProjectRoster(root, { ...options, registry });
|
|
1858
|
+
const file = target.file;
|
|
1859
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
1860
|
+
const base = existing !== null ? sectionedRosterText(existing, file, { ...options, root }) : rosterMarkdownFromPicks(layer.picks);
|
|
1861
|
+
const line = scanRosterMarkdown(base).entries.find((entry) => entry.section === 'team' && !entry.malformed && entry.name.trim().toLowerCase() === member);
|
|
1862
|
+
const old = line ? splitRunsOn(line.value) : { rest: '', on: '' };
|
|
1863
|
+
let rest = old.rest;
|
|
1864
|
+
// "on: muse" named the engine through its place; moving keeps that engine
|
|
1865
|
+
if (!rest && old.on) {
|
|
1866
|
+
const before = readRunsOn(old.on, { isEngine: (words) => canonicalEngineName(words) });
|
|
1867
|
+
if (before.engine) rest = rosterToolLabel(before.engine);
|
|
1868
|
+
}
|
|
1869
|
+
const value = place.here
|
|
1870
|
+
? (rest || (place.engine ? rosterToolLabel(place.engine) : 'on: local'))
|
|
1871
|
+
: [rest, `on: ${place.id}`].filter(Boolean).join(', ');
|
|
1872
|
+
if (place.here && place.engine && rest) {
|
|
1873
|
+
// "on: muse" on a line that already names an engine: the engine changes
|
|
1874
|
+
return { ...setTeamPick(member, place.engine, options, root), place };
|
|
1875
|
+
}
|
|
1876
|
+
writeAtomic(file, replaceTeamLine(base, member, value));
|
|
1877
|
+
if (target.scope === 'session') sweepStaleSessionRosters(options);
|
|
1878
|
+
return { member, value, place, scope: target.scope, file };
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1827
1881
|
function renewRoster(roster, date) {
|
|
1828
1882
|
const next = { ...(roster || {}) };
|
|
1829
1883
|
for (const key of [...ENGINE_ROLES, ...customRosterJobKeys(next)]) {
|
|
@@ -2452,6 +2506,8 @@ module.exports = {
|
|
|
2452
2506
|
customRosterJobKeys,
|
|
2453
2507
|
setRosterPick,
|
|
2454
2508
|
setTeamPick,
|
|
2509
|
+
setTeamPlace,
|
|
2510
|
+
setTeamPlaceCheck,
|
|
2455
2511
|
confirmRoster,
|
|
2456
2512
|
engineHasRole,
|
|
2457
2513
|
engineHosted,
|
package/lib/fleet.js
CHANGED
|
@@ -1866,7 +1866,7 @@ module.exports = {
|
|
|
1866
1866
|
// Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
|
|
1867
1867
|
// not a repo worker, it keeps owning normal mission ticks, not fleet builds.
|
|
1868
1868
|
const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
|
|
1869
|
-
const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable', 'commandcode'];
|
|
1869
|
+
const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable', 'commandcode', 'muse'];
|
|
1870
1870
|
|
|
1871
1871
|
let receiptSequence = 0;
|
|
1872
1872
|
function nowStamp() {
|
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', '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', '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/member-engine.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// builds. A line under "## team" in ROSTER.md overrides that, either with a
|
|
7
7
|
// job ("researcher: search", "fixer: small build") or with engine or model
|
|
8
8
|
// words ("judge: opus"). The job's pick then comes from the roster.
|
|
9
|
+
// An "on:" part says where the member works ("on: atris computer",
|
|
10
|
+
// "on: grok bot"); only a member that runs here gets an engine here.
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const { memberRefusalReason } = require('./member-type');
|
|
@@ -28,6 +30,20 @@ const {
|
|
|
28
30
|
rosterPickFromValue,
|
|
29
31
|
} = require('./engine-registry');
|
|
30
32
|
const { isParkedText } = require('./member-park');
|
|
33
|
+
const { readRunsOn, splitRunsOn, machineName } = require('./runs-on');
|
|
34
|
+
|
|
35
|
+
// The engine id words name, or '' when they name none atris knows.
|
|
36
|
+
function engineNamed(words) {
|
|
37
|
+
try {
|
|
38
|
+
return require('./engine-registry').canonicalEngineName(words) || '';
|
|
39
|
+
} catch { return ''; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The place one team line names, or local when it names none.
|
|
43
|
+
function placeFromOn(on) {
|
|
44
|
+
if (!on) return { id: 'local', label: machineName(), here: true };
|
|
45
|
+
return readRunsOn(on, { isEngine: engineNamed });
|
|
46
|
+
}
|
|
31
47
|
|
|
32
48
|
const SEARCH_WORDS = Object.freeze(['navigator', 'researcher', 'research', 'search', 'scout']);
|
|
33
49
|
const REVIEW_WORDS = Object.freeze(['validator', 'reviewer', 'review', 'judge', 'critic', 'verifier']);
|
|
@@ -111,17 +127,27 @@ function teamLineFor(state, member) {
|
|
|
111
127
|
// member's own kind of work. error is a plain sentence for a line that means
|
|
112
128
|
// neither.
|
|
113
129
|
function interpretTeamValue(value, state, memberKind, options = {}) {
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
let split;
|
|
131
|
+
let place;
|
|
132
|
+
try {
|
|
133
|
+
split = splitRunsOn(value);
|
|
134
|
+
place = placeFromOn(split.on);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return { error: error.message };
|
|
137
|
+
}
|
|
138
|
+
// "on: muse" with nothing else: that engine, here.
|
|
139
|
+
const text = split.rest || (place.engine || '');
|
|
140
|
+
if (!text) return split.on ? { type: 'place', place } : { error: 'names no job or engine' };
|
|
116
141
|
const key = rosterJobKey(text);
|
|
117
|
-
if (key && ENGINE_ROLES.includes(key)) return { type: 'job', key };
|
|
142
|
+
if (key && ENGINE_ROLES.includes(key)) return { type: 'job', key, place };
|
|
118
143
|
const saved = (layer) => Boolean(key && layer && layer.picks && layer.picks[key]);
|
|
119
|
-
if (saved(state.session) || saved(state.project) || saved(state.machine)) return { type: 'job', key };
|
|
144
|
+
if (saved(state.session) || saved(state.project) || saved(state.machine)) return { type: 'job', key, place };
|
|
120
145
|
try {
|
|
121
|
-
return { type: 'pick', pick: rosterPickFromValue(text, ENGINE_JOBS[memberKind], options) };
|
|
146
|
+
return { type: 'pick', pick: rosterPickFromValue(text, ENGINE_JOBS[memberKind], options), place };
|
|
122
147
|
} catch (error) {
|
|
123
|
-
if (key && inferJobKind(text)) return { type: 'job', key };
|
|
124
|
-
|
|
148
|
+
if (key && inferJobKind(text)) return { type: 'job', key, place };
|
|
149
|
+
// the engine part does not read, but the place still counts
|
|
150
|
+
return { error: error.message, place };
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
153
|
|
|
@@ -143,6 +169,7 @@ function resolveEngineForMember(memberName, root = process.cwd(), options = {})
|
|
|
143
169
|
const read = line ? interpretTeamValue(line.value, state, autoKind, options) : null;
|
|
144
170
|
const override = read && !read.error ? read : null;
|
|
145
171
|
const yearless = override && override.type === 'pick' && override.pick.until_needs_year;
|
|
172
|
+
const place = read && read.place ? read.place : placeFromOn('');
|
|
146
173
|
const pickOptions = {
|
|
147
174
|
...options,
|
|
148
175
|
sessionRosterPicks: state.session ? state.session.picks : {},
|
|
@@ -156,6 +183,9 @@ function resolveEngineForMember(memberName, root = process.cwd(), options = {})
|
|
|
156
183
|
source: override ? 'file' : 'auto',
|
|
157
184
|
file: override ? line.file : null,
|
|
158
185
|
line: override ? line.line : null,
|
|
186
|
+
runs_on: place.id,
|
|
187
|
+
runs_on_label: place.label,
|
|
188
|
+
runs_here: place.here,
|
|
159
189
|
...(read && read.error ? { warning: read.error } : {}),
|
|
160
190
|
...(yearless ? { until_warning: `has no year in its until date, so it counts as expired; write until ${yearless}` } : {}),
|
|
161
191
|
};
|
|
@@ -187,6 +217,7 @@ function resolveEngineForMember(memberName, root = process.cwd(), options = {})
|
|
|
187
217
|
const fromRoster = rosterDecided(resolved.source);
|
|
188
218
|
return {
|
|
189
219
|
...base,
|
|
220
|
+
...(override && override.type === 'place' ? { source: 'file', file: line.file, line: line.line } : {}),
|
|
190
221
|
job,
|
|
191
222
|
job_key: jobKey,
|
|
192
223
|
role,
|
|
@@ -214,6 +245,24 @@ function memberRosterEngine(memberName, root = process.cwd(), options = {}) {
|
|
|
214
245
|
}
|
|
215
246
|
}
|
|
216
247
|
|
|
248
|
+
// The place a member works when it is not this machine ("Grok bot",
|
|
249
|
+
// "Atris computer"), or '' when it runs here. Never throws: a broken roster
|
|
250
|
+
// must not stop a run.
|
|
251
|
+
function memberRunsElsewhere(memberName, root = process.cwd(), options = {}) {
|
|
252
|
+
const name = String(memberName || '').trim();
|
|
253
|
+
if (!name || name.includes('/') || name.includes('\\')) return '';
|
|
254
|
+
try {
|
|
255
|
+
// only the member's place is read: no engine is resolved, so nothing is
|
|
256
|
+
// printed or probed for a member that works here
|
|
257
|
+
const line = teamLineFor(options.rosterState || readRosterState(root, options), name);
|
|
258
|
+
if (!line) return '';
|
|
259
|
+
const place = placeFromOn(splitRunsOn(line.value).on);
|
|
260
|
+
return place.here ? '' : place.label || place.id;
|
|
261
|
+
} catch {
|
|
262
|
+
return '';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
217
266
|
// A person or odd-typed card never runs on an engine.
|
|
218
267
|
function memberCardRunnable(root, name) {
|
|
219
268
|
const card = findMemberCard(root, name);
|
|
@@ -254,13 +303,19 @@ function teamRosterView(root = process.cwd(), options = {}) {
|
|
|
254
303
|
warnings.push({ file: line.file, line: line.line, text: line.text, message: `${picked.warning}, so ${name} picks automatically` });
|
|
255
304
|
}
|
|
256
305
|
if (picked.until_warning && line) warnings.push({ file: line.file, line: line.line, text: line.text, message: picked.until_warning });
|
|
306
|
+
// A member working on another machine runs whatever that machine runs,
|
|
307
|
+
// unless its line names the engine; this machine's router has no say.
|
|
308
|
+
const theirs = picked.runs_here === false && picked.pick_source !== 'team';
|
|
257
309
|
return {
|
|
258
310
|
member: name,
|
|
259
311
|
job: picked.job,
|
|
260
|
-
engine: picked.engine ? picked.engine.id : null,
|
|
261
|
-
model: picked.model || null,
|
|
312
|
+
engine: picked.engine && !theirs ? picked.engine.id : null,
|
|
313
|
+
model: theirs ? null : picked.model || null,
|
|
262
314
|
effort: picked.engine && picked.engine.roster_effort ? picked.engine.roster_effort : null,
|
|
263
315
|
max_seconds: picked.engine && picked.engine.roster_max_seconds ? picked.engine.roster_max_seconds : null,
|
|
316
|
+
runs_on: picked.runs_on,
|
|
317
|
+
runs_on_label: picked.runs_on_label,
|
|
318
|
+
runs_here: picked.runs_here,
|
|
264
319
|
source: picked.source,
|
|
265
320
|
file: picked.file,
|
|
266
321
|
reason: picked.reason,
|
|
@@ -271,8 +326,10 @@ function teamRosterView(root = process.cwd(), options = {}) {
|
|
|
271
326
|
|
|
272
327
|
module.exports = {
|
|
273
328
|
autoJobForMember,
|
|
329
|
+
teamLineFor,
|
|
274
330
|
readMemberCard,
|
|
275
331
|
resolveEngineForMember,
|
|
276
332
|
memberRosterEngine,
|
|
333
|
+
memberRunsElsewhere,
|
|
277
334
|
teamRosterView,
|
|
278
335
|
};
|
package/lib/mission-core.js
CHANGED
|
@@ -361,6 +361,13 @@ function memberLogTickReason(result) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
364
|
+
// A member whose team line says it works on another machine is left for
|
|
365
|
+
// that machine, whatever engine the mission names; this one never runs
|
|
366
|
+
// its work in its place.
|
|
367
|
+
if (mission && mission.owner) {
|
|
368
|
+
const elsewhere = require('./member-engine').memberRunsElsewhere(mission.owner, root, options);
|
|
369
|
+
if (elsewhere) return { mission, engine_id: null, requested_engine: null, engine_fallback_reason: null, runs_elsewhere: elsewhere };
|
|
370
|
+
}
|
|
364
371
|
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
365
372
|
// A named engine still gets the roster's time limit for that engine.
|
|
366
373
|
const runner = String(mission && mission.runner || '').trim().toLowerCase();
|
package/lib/model-names.js
CHANGED
|
@@ -32,6 +32,18 @@ function normalizeGrokModel(raw) {
|
|
|
32
32
|
throw new Error(`grok does not know the model "${raw}". use grok 4.7 fast, grok 4.7, or a full grok- id`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Muse takes friendly names: "muse wonder" is muse-wonder-1.0, "spark 1.3"
|
|
36
|
+
// is muse-spark-1.3. Any full muse- id passes through as typed.
|
|
37
|
+
const MUSE_MODEL_SPELLING = /^(?:muse[\s_-]*)?(wonder|spark)(?:[\s_-]*(\d+(?:\.\d+)?))?$/;
|
|
38
|
+
const MUSE_DEFAULT_VERSIONS = Object.freeze({ wonder: '1.0', spark: '1.3' });
|
|
39
|
+
|
|
40
|
+
function normalizeMuseModel(raw) {
|
|
41
|
+
const match = MUSE_MODEL_SPELLING.exec(raw.toLowerCase());
|
|
42
|
+
if (match) return `muse-${match[1]}-${match[2] || MUSE_DEFAULT_VERSIONS[match[1]]}`;
|
|
43
|
+
if (/^muse-[a-z0-9][a-z0-9._-]*$/i.test(raw)) return raw;
|
|
44
|
+
throw new Error(`muse does not know the model "${raw}". use muse wonder, muse spark, or a full muse- id`);
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
// The model to run for a claude id: its replacement when it is retired,
|
|
36
48
|
// else the id as given. Keeps a "[1m]" tail.
|
|
37
49
|
function currentClaudeModel(value) {
|
|
@@ -75,6 +87,7 @@ function normalizeRosterModel(engineId, value) {
|
|
|
75
87
|
const raw = String(value || '').trim();
|
|
76
88
|
if (!raw) return '';
|
|
77
89
|
if (engineId === 'grok') return normalizeGrokModel(raw);
|
|
90
|
+
if (engineId === 'muse') return normalizeMuseModel(raw);
|
|
78
91
|
if (!CLAUDE_FAMILY_ENGINES.includes(engineId)) return raw;
|
|
79
92
|
// "[1m]" asks the claude cli for the long context window; keep it on
|
|
80
93
|
// whatever name it follows.
|
package/lib/runner-command.js
CHANGED
|
@@ -120,6 +120,16 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
120
120
|
effortFlag: '--reasoning-effort',
|
|
121
121
|
efforts: Object.freeze(['low', 'medium', 'high']),
|
|
122
122
|
}),
|
|
123
|
+
// Meta's Muse Code. Its sandbox sends network through a proxy by default,
|
|
124
|
+
// which blocks atris calls; "enabled" keeps the sandbox and approval judge
|
|
125
|
+
// on and lets the run reach the network. Trusting the workspace loads its
|
|
126
|
+
// AGENTS.md rules. Muse runs only Muse models (verified 2026-11-11).
|
|
127
|
+
muse: Object.freeze({
|
|
128
|
+
bin: 'muse',
|
|
129
|
+
model: '',
|
|
130
|
+
ownModelsOnly: true,
|
|
131
|
+
commandTemplate: '{bin} exec --sandbox-network enabled --trust-workspace {pinnedModelFlag} {prompt}',
|
|
132
|
+
}),
|
|
123
133
|
agy: Object.freeze({
|
|
124
134
|
bin: 'agy',
|
|
125
135
|
// No hard pin: the agy CLI keeps its own session model, so an unpinned
|