atris 3.68.3 → 3.68.5

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.
@@ -9925,6 +9925,11 @@ function memberDispatch(...args) {
9925
9925
  if (!asJson) {
9926
9926
  console.log(`${picked.member} (${picked.role}) is taking this because it ${picked.reason.replace(/^[A-Z]/, (c) => c.toLowerCase())}.`);
9927
9927
  }
9928
+ // --ignite means do the work now, the same as the previewed member run;
9929
+ // without it dispatch only opens the mission.
9930
+ if (hasFlag(args, '--ignite') || hasFlag(args, '--wait') || hasFlag(args, '--sync')) {
9931
+ return memberRun(picked.member, ...forwardedArgs);
9932
+ }
9928
9933
  return startMemberRunMission(picked.member, ideaText, forwardedArgs);
9929
9934
  }
9930
9935
 
@@ -10494,16 +10499,21 @@ async function memberGo(...rawArgs) {
10494
10499
  const root = process.cwd();
10495
10500
  const teamDir = path.join(root, 'atris', 'team');
10496
10501
  const coreOrder = ['auto-improver', 'executor', 'navigator', 'validator', 'architect', 'customer-lead', 'researcher'];
10497
- let chosen = 'auto-improver';
10502
+ let chosen = '';
10498
10503
  for (const slug of coreOrder) {
10499
10504
  const mDir = path.join(teamDir, slug);
10500
10505
  if (!fs.existsSync(path.join(mDir, 'MEMBER.md'))) continue;
10501
- const gp = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsFile: path.join(mDir, 'goals.json') });
10506
+ if (!chosen) chosen = slug;
10507
+ const gp = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsJson: path.join(mDir, 'goals.json') });
10502
10508
  if (gp?.goal?.next_push || gp?.goal?.title) {
10503
10509
  chosen = slug;
10504
10510
  break;
10505
10511
  }
10506
10512
  }
10513
+ if (!chosen) {
10514
+ const fallback = findAllMembers(teamDir).filter(isFleetMember)[0];
10515
+ if (fallback) chosen = memberSlugFromRecord(fallback);
10516
+ }
10507
10517
  return memberSteerOrDispatch(chosen, ...rawArgs);
10508
10518
  }
10509
10519
 
@@ -10846,7 +10856,7 @@ async function memberRoom(...args) {
10846
10856
  : [' No work recorded yet. Run: atris member go'];
10847
10857
 
10848
10858
  const lines = [
10849
- 'WORKDAY BRIEFING · TEAM ROOM (100% Autonomous Member Loop)',
10859
+ 'Your team · workday briefing',
10850
10860
  `Engines ready: ${engineSummary}`,
10851
10861
  `Loop: ${room.utopia.loop}`,
10852
10862
  '',
@@ -8,6 +8,7 @@ const { spawn, spawnSync } = require('child_process');
8
8
  const { hasFlag, readFlag, readIntFlag } = require('../lib/arg-parser');
9
9
  const { memberProcessPrompt } = require('../lib/member-context');
10
10
  const { memberIdentityLine, missionObjectiveLine } = require('../lib/member-identity');
11
+ const { formatToolStep, appendProgress, reportProgress } = require('../lib/member-progress');
11
12
  const {
12
13
  compactErrorPayload,
13
14
  compactSuccessPayload,
@@ -2208,6 +2209,10 @@ function missionVerifierCheckedText(verifierResult, mission, guard = null) {
2208
2209
  ? 'Engine verify passed after running the changed surface on this computer.'
2209
2210
  : 'VERIFY FAILED: the engine verify pass did not end with VERDICT: PASS.';
2210
2211
  }
2212
+ // A passing command plus a failing referee must name the referee, not the command.
2213
+ if (verifierResult.referee?.passed === false && verifierResult.status === 0) {
2214
+ return missionVerifierCheckedText(verifierResult.referee, mission, guard);
2215
+ }
2211
2216
  const command = verifierResult.command || mission.verifier || 'configured verifier';
2212
2217
  if (!verifierResult.passed && verifierResult.timed_out) {
2213
2218
  return `VERIFY TIMED OUT: the check ran past its ${verifierTimeoutSeconds(verifierResult)}s window (took ${verifierElapsedSeconds(verifierResult)}s); the code is not failing, the suite is slow. Rerun with more room.`;
@@ -2242,6 +2247,9 @@ function missionVerifierHighLevelTestText(verifierResult, mission, guard = null)
2242
2247
  ? 'The verify engine inspected the tick diff and reported real changed-surface command output.'
2243
2248
  : 'The verify engine did not provide a passing changed-surface verdict.';
2244
2249
  }
2250
+ if (verifierResult.referee?.passed === false && verifierResult.status === 0) {
2251
+ return missionVerifierHighLevelTestText(verifierResult.referee, mission, guard);
2252
+ }
2245
2253
  const command = verifierResult.command || mission.verifier || 'configured verifier';
2246
2254
  if (!verifierResult.passed && verifierResult.timed_out) {
2247
2255
  return `Verifier command timed out after ${verifierElapsedSeconds(verifierResult)}s against a ${verifierTimeoutSeconds(verifierResult)}s window: ${command}. No pass/fail verdict was reached; the suite outran the window rather than the code failing.`;
@@ -9370,6 +9378,9 @@ function spawnClaudeTick(mission, opts) {
9370
9378
  env: { ...gitGuard.env, ATRIS_DISPATCH_DEPTH: String(dispatchDepth.depth + 1) },
9371
9379
  });
9372
9380
  updateMissionLockOwner(missionLock, proc.pid, { phase: 'tick' });
9381
+ const progressMember = mission.owner || 'mission';
9382
+ const seenToolIds = new Set();
9383
+ appendProgress(progressMember, `started: ${mission.objective || ''}`, mission.id);
9373
9384
 
9374
9385
  let stdoutBuf = '';
9375
9386
  let observedSessionIds = new Set();
@@ -9410,6 +9421,14 @@ function spawnClaudeTick(mission, opts) {
9410
9421
  try {
9411
9422
  const ev = JSON.parse(line);
9412
9423
  if (ev.session_id) observedSessionIds.add(ev.session_id);
9424
+ if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
9425
+ for (const block of ev.message.content) {
9426
+ const step = formatToolStep(block, cwd);
9427
+ if (!step || seenToolIds.has(block.id)) continue;
9428
+ if (block.id) seenToolIds.add(block.id);
9429
+ reportProgress(progressMember, step, mission.id);
9430
+ }
9431
+ }
9413
9432
  if (ev.type === 'rate_limit_event' && ev.rate_limit_info) {
9414
9433
  rateLimitInfo = ev.rate_limit_info;
9415
9434
  }
@@ -9436,6 +9455,7 @@ function spawnClaudeTick(mission, opts) {
9436
9455
  if (signal) signal.removeEventListener?.('abort', onAbort);
9437
9456
  updateMissionLockOwner(missionLock, missionLock?.driverPid);
9438
9457
  const ok = code === 0 && !isError && !timedOut && !aborted;
9458
+ appendProgress(progressMember, `finished: ${ok ? 'ok' : 'failed'}`, mission.id);
9439
9459
  const errStr = stderr.slice(-2000);
9440
9460
  const authExpired = !ok && missionRunnerAuthExpired(errStr, finalText, stdoutBuf);
9441
9461
  resolve({
@@ -15,6 +15,21 @@ const {
15
15
  engineTakesModel,
16
16
  engineEffortLevels,
17
17
  } = require('./runner-command');
18
+ // The one model-name table, shared with runner-command: friendly spellings
19
+ // to the ids each cli accepts, plus retired-model upgrades.
20
+ const {
21
+ CLAUDE_FAMILY_ENGINES,
22
+ CLAUDE_MODEL_ALIASES,
23
+ RETIRED_CLAUDE_MODELS,
24
+ CLAUDE_MODEL_SPELLING,
25
+ CLAUDE_LONG_CONTEXT,
26
+ GROK_MODEL_SPELLING,
27
+ currentClaudeModel,
28
+ claudeModelId,
29
+ retiredModelsNamed,
30
+ normalizeRosterModel,
31
+ savedRosterModel,
32
+ } = require('./model-names');
18
33
  // Through the module object so tests can see when ranking runs.
19
34
  const routerBrain = require('./router-brain');
20
35
  const { routerPickExplanation } = routerBrain;
@@ -584,102 +599,6 @@ function rosterPickExpired(pick, now = new Date()) {
584
599
  return localDay(rosterDate(now)).getTime() > until.getTime();
585
600
  }
586
601
 
587
- // Claude-family engines run through the claude CLI, which rejects loose
588
- // spellings like "opus-5.5". Assign saves the id the CLI accepts, and every
589
- // read normalizes again, so older saved picks and hand edits work too.
590
- const CLAUDE_FAMILY_ENGINES = Object.freeze(['claude', 'fable', 'haiku']);
591
- const CLAUDE_MODEL_ALIASES = Object.freeze(['opus', 'sonnet', 'haiku', 'fable', 'opusplan', 'default']);
592
- // A retired model runs as its replacement, so a saved pick keeps working
593
- // after the upgrade. Retiring the next one is one line here.
594
- const RETIRED_CLAUDE_MODELS = Object.freeze({
595
- 'claude-fable-5': 'claude-fable-5-1',
596
- 'claude-haiku-4-5': 'claude-haiku-5-5',
597
- 'claude-haiku-4-5-20251001': 'claude-haiku-5-5',
598
- });
599
- const CLAUDE_MODEL_SPELLING = /^(opus|sonnet|haiku|fable)[\s_-]*(\d+)(?:[\s._-]+(\d+))?$/;
600
- const CLAUDE_LONG_CONTEXT = /\s*\[1m\]$/;
601
-
602
- // Grok takes friendly names too: "grok 4.7 fast" is grok-4.7-build-fast and
603
- // "grok 4.7" is grok-4.7. Any full grok- id passes through as typed.
604
- const GROK_MODEL_SPELLING = /^grok[\s_-]*(\d+(?:\.\d+)?)(?:[\s_-]+(?:build[\s_-]+)?(fast))?$/;
605
-
606
- function normalizeGrokModel(raw) {
607
- const match = GROK_MODEL_SPELLING.exec(raw.toLowerCase());
608
- if (match) return `grok-${match[1]}${match[2] ? '-build-fast' : ''}`;
609
- if (/^grok-[a-z0-9][a-z0-9._-]*$/i.test(raw)) return raw;
610
- throw new Error(`grok does not know the model "${raw}". use grok 4.7 fast, grok 4.7, or a full grok- id`);
611
- }
612
-
613
- // The model to run for a claude id: its replacement when it is retired,
614
- // else the id as given. Keeps a "[1m]" tail.
615
- function currentClaudeModel(value) {
616
- const raw = String(value || '').trim();
617
- const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
618
- const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
619
- const next = RETIRED_CLAUDE_MODELS[base.toLowerCase()];
620
- return next ? `${next}${suffix}` : raw;
621
- }
622
-
623
- // The id the claude cli accepts for a display name typed with a space:
624
- // "opus 5.5" is claude-opus-5-5, "fable 5.1" is claude-fable-5-1. A full id,
625
- // a bare alias ("opus"), a "[1m]" tail and anything else pass through as given
626
- // (a retired id still becomes its replacement).
627
- const CLAUDE_DISPLAY_NAME = /^(opus|sonnet|haiku|fable)\s+(\d+)(?:\.(\d+))?$/i;
628
-
629
- function claudeModelId(value) {
630
- const raw = String(value || '').trim();
631
- const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
632
- const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
633
- const match = CLAUDE_DISPLAY_NAME.exec(base);
634
- if (!match) return currentClaudeModel(raw);
635
- return currentClaudeModel(`claude-${match[1].toLowerCase()}-${match[2]}${match[3] ? `-${match[3]}` : ''}${suffix}`);
636
- }
637
-
638
- // The retired models a roster file's text still names, by id or spelled
639
- // out, each once, so the roster can say they were upgraded.
640
- function retiredModelsNamed(text) {
641
- const found = [];
642
- for (const id of Object.keys(RETIRED_CLAUDE_MODELS)) {
643
- const [, family, major, minor] = /^claude-([a-z]+)-(\d+)-(\d+)/.exec(id) || [];
644
- if (!family) continue;
645
- const spelled = new RegExp(`\\b(?:claude[\\s_-]*)?${family}[\\s_-]*${major}[\\s._-]+${minor}(?![\\d.])`, 'i');
646
- const base = `claude-${family}-${major}-${minor}`;
647
- if (spelled.test(String(text || '')) && !found.includes(base)) found.push(base);
648
- }
649
- return found;
650
- }
651
-
652
- function normalizeRosterModel(engineId, value) {
653
- const raw = String(value || '').trim();
654
- if (!raw) return '';
655
- if (engineId === 'grok') return normalizeGrokModel(raw);
656
- if (!CLAUDE_FAMILY_ENGINES.includes(engineId)) return raw;
657
- // "[1m]" asks the claude cli for the long context window; keep it on
658
- // whatever name it follows.
659
- const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
660
- const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
661
- const lower = base.toLowerCase();
662
- if (CLAUDE_MODEL_ALIASES.includes(lower)) return `${lower}${suffix}`;
663
- const match = CLAUDE_MODEL_SPELLING.exec(lower.replace(/^claude[\s_-]*/, ''));
664
- if (match) {
665
- const id = `claude-${match[1]}-${match[2]}${match[3] ? `-${match[3]}` : ''}`;
666
- return currentClaudeModel(`${id}${suffix}`);
667
- }
668
- if (lower.startsWith('claude-')) return currentClaudeModel(`${base}${suffix}`);
669
- throw new Error(`${engineId} does not know the model "${raw}". use opus, sonnet, haiku, fable, opusplan, default, a version like opus 5.5, sonnet 5, haiku 5.5, or fable 5.1, or a full claude- id; add [1m] for the long context window`);
670
- }
671
-
672
- // A saved pick's model, normalized the same way assign does. null means the
673
- // saved name is one the claude cli would reject, so the pick cannot run.
674
- function savedRosterModel(engineId, value) {
675
- try {
676
- return normalizeRosterModel(engineId, value);
677
- } catch {
678
- return null;
679
- }
680
- }
681
-
682
-
683
602
  // A registry saved before an engine learned a job keeps its old role list;
684
603
  // the roster still honors the job the engine can do today, including a job
685
604
  // it only takes by roster pick.
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ // Live progress for a working member: one plain line per tool step, printed to
4
+ // stderr and appended to <dir>/<member>.log so another shell can tail it.
5
+
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ function realish(p) {
11
+ try { return fs.realpathSync(p); } catch {}
12
+ try { return path.join(fs.realpathSync(path.dirname(p)), path.basename(p)); } catch {}
13
+ return p;
14
+ }
15
+
16
+ function insideRel(cwd, p) {
17
+ const rel = path.relative(cwd, p);
18
+ return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : null;
19
+ }
20
+
21
+ // Relative to cwd when inside it; symlinked roots (macOS /var vs /private/var)
22
+ // still count as inside.
23
+ function relativeTo(cwd, file) {
24
+ const p = String(file || '');
25
+ if (!p || !cwd || !path.isAbsolute(p)) return p;
26
+ return insideRel(cwd, p) || insideRel(realish(cwd), realish(p)) || p;
27
+ }
28
+
29
+ function formatToolStep(block, cwd) {
30
+ if (!block || block.type !== 'tool_use') return null;
31
+ const name = String(block.name || 'tool');
32
+ const input = block.input || {};
33
+ switch (name) {
34
+ case 'Read':
35
+ return `reading ${relativeTo(cwd, input.file_path)}`;
36
+ case 'Edit':
37
+ case 'MultiEdit':
38
+ case 'Write':
39
+ return `editing ${relativeTo(cwd, input.file_path)}`;
40
+ case 'Bash': {
41
+ const first = String(input.command || '').split('\n')[0].trim();
42
+ return `running: ${first.slice(0, 70)}`;
43
+ }
44
+ case 'Grep':
45
+ return `searching for ${input.pattern || ''}`;
46
+ case 'Glob':
47
+ return `finding ${input.pattern || ''}`;
48
+ default:
49
+ return name.toLowerCase();
50
+ }
51
+ }
52
+
53
+ function progressDir() {
54
+ return process.env.ATRIS_PROGRESS_DIR || path.join(os.homedir(), '.atris', 'progress');
55
+ }
56
+
57
+ function safeMember(member) {
58
+ return String(member || 'mission').replace(/[^A-Za-z0-9._-]/g, '_') || 'mission';
59
+ }
60
+
61
+ function hhmm(date = new Date()) {
62
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
63
+ }
64
+
65
+ function appendProgress(member, line, missionId) {
66
+ // Test runs never write into the real home folder unless they name a dir.
67
+ if (!process.env.ATRIS_PROGRESS_DIR && process.env.NODE_TEST_CONTEXT) return;
68
+ try {
69
+ const dir = progressDir();
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ const entry = `${new Date().toISOString()} ${missionId || '-'} ${line}\n`;
72
+ fs.appendFileSync(path.join(dir, `${safeMember(member)}.log`), entry, 'utf8');
73
+ } catch {}
74
+ }
75
+
76
+ // Print one step for the operator and keep it in the member's progress log.
77
+ function reportProgress(member, line, missionId) {
78
+ try { process.stderr.write(` ${member || 'mission'} ${hhmm()} ${line}\n`); } catch {}
79
+ appendProgress(member, line, missionId);
80
+ }
81
+
82
+ module.exports = { formatToolStep, appendProgress, reportProgress };
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // Friendly model names to the ids each engine's cli accepts. This is the ONE
4
+ // table of spellings: engine-registry checks roster picks with it and
5
+ // runner-command normalizes launch models with it. "opus 5.5" is
6
+ // claude-opus-5-5, "grok 4.7 fast" is grok-4.7-build-fast. Kept free of
7
+ // requires so both files can load it without a circular dependency.
8
+
9
+ // Claude-family engines run through the claude CLI, which rejects loose
10
+ // spellings like "opus-5.5". Assign saves the id the CLI accepts, and every
11
+ // read normalizes again, so older saved picks and hand edits work too.
12
+ const CLAUDE_FAMILY_ENGINES = Object.freeze(['claude', 'fable', 'haiku']);
13
+ const CLAUDE_MODEL_ALIASES = Object.freeze(['opus', 'sonnet', 'haiku', 'fable', 'opusplan', 'default']);
14
+ // A retired model runs as its replacement, so a saved pick keeps working
15
+ // after the upgrade. Retiring the next one is one line here.
16
+ const RETIRED_CLAUDE_MODELS = Object.freeze({
17
+ 'claude-fable-5': 'claude-fable-5-1',
18
+ 'claude-haiku-4-5': 'claude-haiku-5-5',
19
+ 'claude-haiku-4-5-20251001': 'claude-haiku-5-5',
20
+ });
21
+ const CLAUDE_MODEL_SPELLING = /^(opus|sonnet|haiku|fable)[\s_-]*(\d+)(?:[\s._-]+(\d+))?$/;
22
+ const CLAUDE_LONG_CONTEXT = /\s*\[1m\]$/;
23
+
24
+ // Grok takes friendly names too: "grok 4.7 fast" is grok-4.7-build-fast and
25
+ // "grok 4.7" is grok-4.7. Any full grok- id passes through as typed.
26
+ const GROK_MODEL_SPELLING = /^grok[\s_-]*(\d+(?:\.\d+)?)(?:[\s_-]+(?:build[\s_-]+)?(fast))?$/;
27
+
28
+ function normalizeGrokModel(raw) {
29
+ const match = GROK_MODEL_SPELLING.exec(raw.toLowerCase());
30
+ if (match) return `grok-${match[1]}${match[2] ? '-build-fast' : ''}`;
31
+ if (/^grok-[a-z0-9][a-z0-9._-]*$/i.test(raw)) return raw;
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
+ }
34
+
35
+ // The model to run for a claude id: its replacement when it is retired,
36
+ // else the id as given. Keeps a "[1m]" tail.
37
+ function currentClaudeModel(value) {
38
+ const raw = String(value || '').trim();
39
+ const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
40
+ const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
41
+ const next = RETIRED_CLAUDE_MODELS[base.toLowerCase()];
42
+ return next ? `${next}${suffix}` : raw;
43
+ }
44
+
45
+ // The id the claude cli accepts for a display name typed with a space:
46
+ // "opus 5.5" is claude-opus-5-5, "fable 5.1" is claude-fable-5-1. A full id,
47
+ // a bare alias ("opus"), a "[1m]" tail and anything else pass through as given
48
+ // (a retired id still becomes its replacement).
49
+ const CLAUDE_DISPLAY_NAME = /^(opus|sonnet|haiku|fable)\s+(\d+)(?:\.(\d+))?$/i;
50
+
51
+ function claudeModelId(value) {
52
+ const raw = String(value || '').trim();
53
+ const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
54
+ const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
55
+ const match = CLAUDE_DISPLAY_NAME.exec(base);
56
+ if (!match) return currentClaudeModel(raw);
57
+ return currentClaudeModel(`claude-${match[1].toLowerCase()}-${match[2]}${match[3] ? `-${match[3]}` : ''}${suffix}`);
58
+ }
59
+
60
+ // The retired models a roster file's text still names, by id or spelled
61
+ // out, each once, so the roster can say they were upgraded.
62
+ function retiredModelsNamed(text) {
63
+ const found = [];
64
+ for (const id of Object.keys(RETIRED_CLAUDE_MODELS)) {
65
+ const [, family, major, minor] = /^claude-([a-z]+)-(\d+)-(\d+)/.exec(id) || [];
66
+ if (!family) continue;
67
+ const spelled = new RegExp(`\\b(?:claude[\\s_-]*)?${family}[\\s_-]*${major}[\\s._-]+${minor}(?![\\d.])`, 'i');
68
+ const base = `claude-${family}-${major}-${minor}`;
69
+ if (spelled.test(String(text || '')) && !found.includes(base)) found.push(base);
70
+ }
71
+ return found;
72
+ }
73
+
74
+ function normalizeRosterModel(engineId, value) {
75
+ const raw = String(value || '').trim();
76
+ if (!raw) return '';
77
+ if (engineId === 'grok') return normalizeGrokModel(raw);
78
+ if (!CLAUDE_FAMILY_ENGINES.includes(engineId)) return raw;
79
+ // "[1m]" asks the claude cli for the long context window; keep it on
80
+ // whatever name it follows.
81
+ const suffix = CLAUDE_LONG_CONTEXT.test(raw) ? '[1m]' : '';
82
+ const base = raw.replace(CLAUDE_LONG_CONTEXT, '').trim();
83
+ const lower = base.toLowerCase();
84
+ if (CLAUDE_MODEL_ALIASES.includes(lower)) return `${lower}${suffix}`;
85
+ const match = CLAUDE_MODEL_SPELLING.exec(lower.replace(/^claude[\s_-]*/, ''));
86
+ if (match) {
87
+ const id = `claude-${match[1]}-${match[2]}${match[3] ? `-${match[3]}` : ''}`;
88
+ return currentClaudeModel(`${id}${suffix}`);
89
+ }
90
+ if (lower.startsWith('claude-')) return currentClaudeModel(`${base}${suffix}`);
91
+ throw new Error(`${engineId} does not know the model "${raw}". use opus, sonnet, haiku, fable, opusplan, default, a version like opus 5.5, sonnet 5, haiku 5.5, or fable 5.1, or a full claude- id; add [1m] for the long context window`);
92
+ }
93
+
94
+ // A saved pick's model, normalized the same way assign does. null means the
95
+ // saved name is one the claude cli would reject, so the pick cannot run.
96
+ function savedRosterModel(engineId, value) {
97
+ try {
98
+ return normalizeRosterModel(engineId, value);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ module.exports = {
105
+ CLAUDE_FAMILY_ENGINES,
106
+ CLAUDE_MODEL_ALIASES,
107
+ RETIRED_CLAUDE_MODELS,
108
+ CLAUDE_MODEL_SPELLING,
109
+ CLAUDE_LONG_CONTEXT,
110
+ GROK_MODEL_SPELLING,
111
+ currentClaudeModel,
112
+ claudeModelId,
113
+ retiredModelsNamed,
114
+ normalizeRosterModel,
115
+ savedRosterModel,
116
+ };
@@ -16,19 +16,20 @@ const DEFAULT_CLAUDE_RUNNER_BIN = 'claude';
16
16
  const EFFORT_WORDS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
17
17
  const CLAUDE_EFFORTS = EFFORT_WORDS;
18
18
 
19
- const FRIENDLY_CLAUDE_MODEL_ALIASES = Object.freeze({
20
- 'opus 5.5': 'claude-opus-5-5',
21
- 'opus-5.5': 'claude-opus-5-5',
22
- 'opus 5': 'claude-opus-5',
23
- 'opus-5': 'claude-opus-5',
24
- 'opus 4.8': 'claude-opus-4-8',
25
- 'opus-4.8': 'claude-opus-4-8',
26
- });
19
+ // The one model-name table lives in model-names.js, shared with
20
+ // engine-registry. Friendly names become the ids the claude cli accepts;
21
+ // anything it does not know passes through unchanged (runners like glm and
22
+ // swe name their own models here, so this never throws).
23
+ const { claudeModelId, currentClaudeModel, normalizeRosterModel } = require('./model-names');
27
24
 
28
25
  function normalizeClaudeModelName(value) {
29
26
  const trimmed = String(value || '').trim();
30
27
  if (!trimmed) return '';
31
- return FRIENDLY_CLAUDE_MODEL_ALIASES[trimmed.toLowerCase()] || trimmed;
28
+ try {
29
+ return normalizeRosterModel('claude', trimmed);
30
+ } catch {
31
+ return trimmed;
32
+ }
32
33
  }
33
34
 
34
35
  // Canonical runner profiles. Each entry is the config an operator would pick
@@ -246,7 +247,7 @@ function ownModelsOnlyEngineForBin(bin) {
246
247
  // claude env pin) and rides its own default instead.
247
248
  function runnerModelFor(engineId, model) {
248
249
  // Required here, not at the top: engine-registry requires this file.
249
- const { isClaudeFamilyModel, currentClaudeModel, claudeModelId } = require('./engine-registry');
250
+ const { isClaudeFamilyModel } = require('./engine-registry');
250
251
  const name = String(engineId || '').trim().toLowerCase();
251
252
  // A display name like "opus 5.5" only becomes an id for the claude cli (or
252
253
  // an unnamed engine). Any other engine's model name is handed over as is.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.68.3",
3
+ "version": "3.68.5",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {