@polderlabs/bizar 10.23.14 → 10.23.16

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/cli/bin.mjs CHANGED
@@ -129,6 +129,7 @@ function showHelp() {
129
129
  workflow <subcommand> Session-bound autopilot workflow state
130
130
  hook <name> Run a portable Claude Code hook
131
131
  worktree-merge <branch> Merge a feature branch with archive tag (no work lost)
132
+ worker <subcommand> Run an exact-model Claude process worker in a worktree
132
133
  models Configure the global model picker and Models.dev metadata
133
134
  evidence <subcommand> Inspect model-routing evidence
134
135
  improve <subcommand> Propose and verify bounded self-edits
@@ -613,6 +614,14 @@ async function main() {
613
614
  return;
614
615
  }
615
616
 
617
+ case 'worker': {
618
+ const mod = await importCommand('worker');
619
+ if (!mod) { process.exit(EXIT_ERROR); return; }
620
+ const found = await mod.run(cmd, cmdArgs, isHelpRequest);
621
+ if (found === false) process.exit(EXIT_USAGE);
622
+ break;
623
+ }
624
+
616
625
  case 'explain-run': {
617
626
  const mod = await importCommand('explain-run');
618
627
  if (!mod) {
@@ -18,7 +18,7 @@
18
18
  * reject user-selected IDs.
19
19
  */
20
20
  import chalk from 'chalk';
21
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
22
22
  import { dirname, join } from 'node:path';
23
23
  import readline from 'node:readline';
24
24
 
@@ -991,6 +991,10 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = [],
991
991
  // must be keys; configured gateway aliases are values. This also suppresses
992
992
  // print-mode `[claude-code:unrecognized_model]` diagnostics for Agent SDK calls.
993
993
  settings.modelOverrides = buildClaudeModelOverrides(synced);
994
+ const nativeAgentAliases = applyNativeAgentAliasTargets(settings, synced);
995
+ const generatedAgents = syncGeneratedModelAgents(synced, {
996
+ agentsDir: settingsJsonPath === undefined ? undefined : join(dirname(path), 'agents', 'bizar-models'),
997
+ });
994
998
  if (requiresGatewayModelDiscovery(synced)) {
995
999
  settings.env = {
996
1000
  ...(settings.env || {}),
@@ -1020,6 +1024,8 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = [],
1020
1024
  syncedIds: synced,
1021
1025
  skippedStale: skipped,
1022
1026
  skippedDisabled,
1027
+ nativeAgentAliases,
1028
+ generatedAgents,
1023
1029
  settingsPath: path,
1024
1030
  };
1025
1031
  }
@@ -1042,9 +1048,8 @@ export function configuredEnabledModels(router) {
1042
1048
  }
1043
1049
 
1044
1050
  export const CLAUDE_MODEL_OVERRIDE_KEYS = Object.freeze([
1045
- 'claude-fable-5',
1046
- 'claude-opus-5',
1047
1051
  'claude-sonnet-5',
1052
+ 'claude-opus-5',
1048
1053
  'claude-haiku-4-5-20251001',
1049
1054
  'claude-opus-4-8',
1050
1055
  'claude-opus-4-7',
@@ -1060,13 +1065,78 @@ export const CLAUDE_MODEL_OVERRIDE_KEYS = Object.freeze([
1060
1065
  'claude-3-5-sonnet-20241022',
1061
1066
  ]);
1062
1067
 
1068
+ // Claude Code's native Agent tool accepts these transport labels in the
1069
+ // installed runtime. They are labels only: Bizar binds them to the operator's
1070
+ // gateway IDs below; they never select an Anthropic provider by themselves.
1071
+ export const NATIVE_AGENT_ALIASES = Object.freeze(['sonnet', 'opus', 'haiku', 'fable']);
1072
+
1073
+ const MODEL_AGENT_WORDS = Object.freeze({ a:'alpha', b:'bravo', c:'charlie', d:'delta', e:'echo', f:'foxtrot', g:'golf', h:'hotel', i:'india', j:'juliet', k:'kilo', l:'lima', m:'mike', n:'november', o:'oscar', p:'papa', q:'quebec', r:'romeo', s:'sierra', t:'tango', u:'uniform', v:'victor', w:'whiskey', x:'xray', y:'yankee', z:'zulu', 0:'zero', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7:'seven', 8:'eight', 9:'nine', '/':'slash', '.':'dot', '-':'dash', '_':'under' });
1074
+
1075
+ export function modelAgentName(modelId) {
1076
+ return `bizar-model-${[...String(modelId || '').toLowerCase()].map((ch) => MODEL_AGENT_WORDS[ch] || 'unknown').join('-')}`;
1077
+ }
1078
+
1079
+ export function syncGeneratedModelAgents(modelIds, opts = {}) {
1080
+ const agentsDir = opts.agentsDir || join(resolveClaudeConfigDir(), 'agents', 'bizar-models');
1081
+ const ids = [...new Set((Array.isArray(modelIds) ? modelIds : []).filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim()))];
1082
+ if (existsSync(agentsDir)) rmSync(agentsDir, { recursive: true, force: true });
1083
+ if (!ids.length) return { agentsDir, names: [] };
1084
+ mkdirSync(agentsDir, { recursive: true, mode: 0o700 });
1085
+ const names = ids.map((id) => modelAgentName(id));
1086
+ ids.forEach((id, index) => writeFileSync(join(agentsDir, `${names[index]}.md`), `---\nname: ${names[index]}\ndescription: Bizar configured gateway model worker.\nmodel: ${id}\n---\n\nFollow the assigned Bizar role and task. Report concise evidence to the coordinator.\n`, { mode: 0o600 }));
1087
+ return { agentsDir, names };
1088
+ }
1089
+
1090
+ const NATIVE_AGENT_OVERRIDE_KEYS = Object.freeze({
1091
+ sonnet: 'claude-sonnet-5',
1092
+ opus: 'claude-opus-5',
1093
+ haiku: 'claude-haiku-4-5-20251001',
1094
+ fable: 'claude-fable-5',
1095
+ });
1096
+
1097
+ export function buildNativeAgentAliasTargets(modelIds) {
1098
+ const unique = [...new Set((Array.isArray(modelIds) ? modelIds : [])
1099
+ .filter((id) => typeof id === 'string' && id.trim())
1100
+ .map((id) => id.trim()))];
1101
+ if (unique.length === 0) return {};
1102
+ return Object.fromEntries(NATIVE_AGENT_ALIASES.map((alias, index) => [
1103
+ alias,
1104
+ unique[index] || unique[0],
1105
+ ]));
1106
+ }
1107
+
1108
+ export function applyNativeAgentAliasTargets(settings, modelIds) {
1109
+ const targets = buildNativeAgentAliasTargets(modelIds);
1110
+ const env = { ...(settings.env || {}) };
1111
+ const envKeys = {
1112
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
1113
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
1114
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
1115
+ fable: 'ANTHROPIC_DEFAULT_FABLE_MODEL',
1116
+ };
1117
+ for (const alias of NATIVE_AGENT_ALIASES) {
1118
+ const envKey = envKeys[alias];
1119
+ if (targets[alias]) env[envKey] = targets[alias];
1120
+ else delete env[envKey];
1121
+ }
1122
+ settings.env = env;
1123
+ return targets;
1124
+ }
1125
+
1063
1126
  export function buildClaudeModelOverrides(modelIds) {
1064
1127
  const unique = [...new Set((Array.isArray(modelIds) ? modelIds : [])
1065
1128
  .filter((id) => typeof id === 'string' && id.trim())
1066
1129
  .map((id) => id.trim()))];
1067
1130
  if (unique.length === 0) return {};
1068
- return Object.fromEntries(CLAUDE_MODEL_OVERRIDE_KEYS
1069
- .map((key, index) => [key, unique[index % unique.length]]));
1131
+ const targets = buildNativeAgentAliasTargets(unique);
1132
+ const nativeEntries = NATIVE_AGENT_ALIASES.map((alias) => [
1133
+ NATIVE_AGENT_OVERRIDE_KEYS[alias],
1134
+ targets[alias],
1135
+ ]);
1136
+ const remaining = CLAUDE_MODEL_OVERRIDE_KEYS
1137
+ .filter((key) => !Object.values(NATIVE_AGENT_OVERRIDE_KEYS).includes(key))
1138
+ .map((key, index) => [key, unique[index % unique.length]]);
1139
+ return Object.fromEntries([...nativeEntries, ...remaining]);
1070
1140
  }
1071
1141
 
1072
1142
  /** Custom gateway IDs must be discoverable to Claude's SDK/subagent path. */
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Exact-model process workers.
3
+ *
4
+ * This command is the separately launched alternative: it creates an isolated
5
+ * worktree and starts a top-level Claude process with the literal gateway ID
6
+ * selected through `bizar models`. No global settings are rewritten per
7
+ * worker, so parallel workers cannot race each other's model selection.
8
+ */
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
11
+ import { basename, dirname, join } from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { resolveBizarHome, resolveClaudeConfigDir, resolveGlobalModelRouter } from '../config-paths.mjs';
14
+
15
+ function runGit(args, cwd) {
16
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
17
+ if (result.status !== 0) throw new Error((result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim());
18
+ return result.stdout.trim();
19
+ }
20
+
21
+ function readRouter() {
22
+ const path = resolveGlobalModelRouter();
23
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { throw new Error('Global Bizar model router is missing or invalid; run `bizar models`.'); }
24
+ }
25
+
26
+ export function enabledModels(router) {
27
+ const disabled = new Set((router.disabledProviders || []).filter((id) => typeof id === 'string').map((id) => id.trim().toLowerCase()));
28
+ return (router.userSelected?.models || []).filter((id) => typeof id === 'string' && id.trim() && ![...disabled].some((prefix) => id.toLowerCase().startsWith(prefix)));
29
+ }
30
+
31
+ function workerRoot() { return join(resolveBizarHome(), 'workers'); }
32
+ function statePath(id) { return join(workerRoot(), `${id}.json`); }
33
+ function writeState(state) {
34
+ mkdirSync(workerRoot(), { recursive: true, mode: 0o700 });
35
+ writeFileSync(statePath(state.id), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
36
+ }
37
+
38
+ export function buildWorkerPlan({ repoRoot = process.cwd(), model, task, agent = 'todd' } = {}) {
39
+ if (!model || typeof model !== 'string') throw new Error('worker start requires --model <selected gateway model id>.');
40
+ if (!task || typeof task !== 'string') throw new Error('worker start requires --task <bounded task>.');
41
+ const router = readRouter();
42
+ if (!enabledModels(router).includes(model)) throw new Error(`Model ${model} is not an enabled global bizar models selection.`);
43
+ const root = runGit(['rev-parse', '--show-toplevel'], repoRoot);
44
+ const id = `worker-${randomUUID().slice(0, 8)}`;
45
+ const branch = `wt/${id}`;
46
+ const worktree = join(dirname(root), `${basename(root)}-${id}`);
47
+ const logPath = join(workerRoot(), `${id}.log`);
48
+ return { id, root, branch, worktree, model, task: task.trim(), agent, logPath };
49
+ }
50
+
51
+ export function workerClaudeArgs(plan) {
52
+ return [
53
+ '--print', '--name', `bizar-${plan.id}`, '--model', plan.model,
54
+ '--permission-mode', 'acceptEdits', '--agent', plan.agent,
55
+ `${plan.task}\n\nYou are an exact-model Bizar process worker in ${plan.worktree}. Work only in this worktree. Do not spawn subagents. Implement and test the bounded task, commit one logical change locally, never push, then report the commit and verification.`,
56
+ ];
57
+ }
58
+
59
+ function parseStart(args) {
60
+ const value = (name) => {
61
+ const eq = args.find((arg) => arg.startsWith(`${name}=`));
62
+ if (eq) return eq.slice(name.length + 1);
63
+ const index = args.indexOf(name);
64
+ return index >= 0 ? args[index + 1] : null;
65
+ };
66
+ return { model: value('--model'), task: value('--task'), agent: value('--agent') || 'todd', background: args.includes('--background') };
67
+ }
68
+
69
+ function listWorkers() {
70
+ if (!existsSync(workerRoot())) return [];
71
+ return readdirSync(workerRoot()).filter((name) => name.endsWith('.json')).flatMap((name) => {
72
+ try { return [JSON.parse(readFileSync(join(workerRoot(), name), 'utf8'))]; } catch { return []; }
73
+ }).sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')));
74
+ }
75
+
76
+ function usage() {
77
+ console.log(`
78
+ bizar worker start --model <selected-id> --task <task> [--agent todd] [--background]
79
+ bizar worker list [--json]
80
+
81
+ Starts an isolated top-level Claude Code process in a wt/ worktree with the
82
+ exact selected gateway model. Use this when a separately launched worktree
83
+ process is useful. Merge completed branches with bizar worktree-merge <branch>.
84
+ `);
85
+ }
86
+
87
+ export async function run(name, args, isHelpRequest) {
88
+ if (name !== 'worker') return false;
89
+ if (isHelpRequest || args.length === 0) { usage(); return true; }
90
+ const [subcommand] = args;
91
+ if (subcommand === 'list') {
92
+ const rows = listWorkers();
93
+ if (args.includes('--json')) process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
94
+ else for (const row of rows) console.log(`${row.id}\t${row.status}\t${row.model}\t${row.branch}`);
95
+ return true;
96
+ }
97
+ if (subcommand !== 'start') { usage(); return false; }
98
+ const parsed = parseStart(args.slice(1));
99
+ const plan = buildWorkerPlan({ repoRoot: process.cwd(), ...parsed });
100
+ mkdirSync(workerRoot(), { recursive: true, mode: 0o700 });
101
+ runGit(['worktree', 'add', '-b', plan.branch, plan.worktree, 'HEAD'], plan.root);
102
+ const state = { ...plan, status: 'running', startedAt: new Date().toISOString(), pid: null };
103
+ const child = spawn(process.env.CLAUDE_BIN || 'claude', workerClaudeArgs(plan), {
104
+ cwd: plan.worktree,
105
+ env: { ...process.env, CLAUDE_CONFIG_DIR: resolveClaudeConfigDir() },
106
+ detached: parsed.background,
107
+ stdio: parsed.background ? ['ignore', 'pipe', 'pipe'] : 'inherit',
108
+ });
109
+ child.once('error', (error) => {
110
+ writeState({ ...state, status: 'failed', exitedAt: new Date().toISOString(), error: error.message });
111
+ console.error(`Unable to start exact-model worker: ${error.message}`);
112
+ });
113
+ state.pid = child.pid || null;
114
+ writeState(state);
115
+ child.once('exit', (code) => writeState({ ...state, status: code === 0 ? 'completed' : 'failed', exitedAt: new Date().toISOString(), exitCode: code ?? 1 }));
116
+ if (parsed.background) {
117
+ const log = createWriteStream(plan.logPath, { flags: 'a', mode: 0o600 });
118
+ child.stdout?.pipe(log); child.stderr?.pipe(log); child.unref();
119
+ console.log(JSON.stringify({ id: plan.id, status: 'running', model: plan.model, branch: plan.branch, worktree: plan.worktree }));
120
+ return true;
121
+ }
122
+ const code = await new Promise((done) => child.once('exit', (value) => done(value ?? 1)));
123
+ process.exitCode = code;
124
+ return true;
125
+ }
@@ -17,7 +17,7 @@ default for meaningful work.
17
17
  | Shape | Signals | Execution |
18
18
  |---|---|---|
19
19
  | Tiny direct | one obvious copy, typo, comment, whitespace, or single style-token edit; one target; no behavior or test change | inspect, make the micro-edit, run the smallest proving check yourself |
20
- | Single isolated worker | one bounded implementation after scope is clear | dispatch one worktree-isolated Agent with an explicit Bizar model; integrate and verify |
20
+ | Single isolated worker | one bounded implementation after scope is clear | dispatch one worktree-isolated native Agent with its explicit Bizar model; use an exact-model process worker only when a separate Claude process is useful; integrate and verify |
21
21
  | Native workflow | repeatable diagnosis, research, review, or an implementation needing visible phase barriers | invoke the matching Bizar workflow with explicit Bizar routing |
22
22
  | Agent team | three or more sustained, independent roles need bounded cross-talk or coordinated handoff | use the native Agent-team capability; writers use worktrees and explicit Bizar models |
23
23
  | Parallel agents | two disjoint writable scopes with no cross-talk needed | dispatch concurrently with explicit models and worktree isolation |
@@ -34,15 +34,18 @@ version-sensitive claims. Inspect installed skills before hard or specialized
34
34
  work; if stuck with no match, search skills.sh and review the candidate before
35
35
  proposing installation.
36
36
 
37
- Before every Workflow, Agent, or Agent-team call, read the global Bizar model router and construct a
38
- small `args.routing` object whose `default`, `medium`, and `high` values are
39
- explicit enabled configured model IDs (user picks win; otherwise use enabled
40
- tier candidates). Include the user's task in the same args object under the
41
- workflow's documented task field. Pass the exact enabled raw gateway ID chosen
42
- from `bizar models` as `model`; never substitute a Claude family alias. You may
43
- include that selection in `additionalContext.bizarConfiguredModel` for audit
44
- telemetry. If no configured model exists, stop and ask the operator to run
45
- `bizar models`; never retry by omitting `model` or using `inherit`.
37
+ Before every Workflow, Agent, or Agent-team call, read the global Bizar model
38
+ router and construct a small `args.routing` object whose `default`, `medium`,
39
+ and `high` values are explicit enabled configured gateway IDs. Include the
40
+ user's task in the same args object under the workflow's documented task field.
41
+ Use the generated `bizar-models` user agent matching the chosen full gateway
42
+ ID as `subagent_type`, and omit the native Agent `model` parameter entirely.
43
+ That definition's frontmatter owns the full-ID selection for ordinary agents,
44
+ workflows, and teams. Include the raw ID in `additionalContext.bizarConfiguredModel`
45
+ for audit telemetry. Never use `inherit` or an unconfigured provider default.
46
+ `sonnet`, `opus`, `haiku`, and `fable` are compatibility aliases only. If no
47
+ generated configured definition exists, stop and ask the operator to run
48
+ `bizar models`; never omit the generated agent type or cycle providers.
46
49
 
47
50
  Invoke the selected workflow by `name` first. If Claude reports that the Bizar
48
51
  name is unavailable, resolve the active Claude config directory and retry once
@@ -55,14 +58,16 @@ implementation around a broken workflow installation.
55
58
 
56
59
  ## Models
57
60
 
58
- For every Agent call, select the cheapest sufficient enabled configured model
61
+ For every dispatch, select the cheapest sufficient enabled configured model
59
62
  from the global Bizar router. User-selected models take precedence over tier
60
- candidates; `disabledProviders` excludes both. Pass that raw selected custom
61
- ID directly; the guard verifies it is in the enabled selected pool. If no
62
- enabled configured candidate exists, stop with the configuration error. Never
63
- let Claude choose an unconfigured default, inherit the session model, or retry
64
- by cycling models, providers, or tiers. A single configured transport failover
65
- is allowed only when the router explicitly supplies it.
63
+ candidates; `disabledProviders` excludes both. Native aliases are transport
64
+ labels bound by `bizar models`, not Anthropic selections. Use the full selected
65
+ model ID directly for native Agents and teams; every enabled selection is
66
+ eligible. Use `bizar worker` only when a separately launched process worktree
67
+ is useful. If no enabled configured candidate exists, stop with the
68
+ configuration error. Never let Claude choose an unconfigured default, inherit
69
+ the session model, use an unmapped alias,
70
+ or retry by cycling models, providers, or tiers.
66
71
 
67
72
  ## Worktree Discipline and integration
68
73
 
@@ -49,6 +49,7 @@ import { pathToFileURL } from 'node:url';
49
49
 
50
50
  import { loadModelRouter } from '../../../config/agents/model-assignment.mjs';
51
51
  import { resolveClaudeConfigDir } from '../../../cli/config-paths.mjs';
52
+ import { modelAgentName } from '../../../cli/commands/models.mjs';
52
53
 
53
54
  function deny(reason) {
54
55
  return {
@@ -169,21 +170,32 @@ const NATIVE_AGENT_TRANSPORT_KEYS = Object.freeze({
169
170
  sonnet: 'claude-sonnet-5',
170
171
  opus: 'claude-opus-5',
171
172
  haiku: 'claude-haiku-4-5-20251001',
173
+ fable: 'claude-fable-5',
174
+ });
175
+
176
+ const NATIVE_AGENT_TRANSPORT_ENV_KEYS = Object.freeze({
177
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
178
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
179
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
180
+ fable: 'ANTHROPIC_DEFAULT_FABLE_MODEL',
172
181
  });
173
182
 
174
183
  function readTransportTarget(alias, options = {}) {
175
184
  const key = NATIVE_AGENT_TRANSPORT_KEYS[alias];
176
185
  if (!key) return '';
177
- const overrides = options.modelOverrides || (() => {
186
+ const settings = options.settings || (options.modelOverrides ? null : (() => {
178
187
  const settingsPath = options.settingsPath || join(resolveClaudeConfigDir(), 'settings.json');
179
- try { return JSON.parse(readFileSync(settingsPath, 'utf8'))?.modelOverrides; } catch { return null; }
180
- })();
188
+ try { return JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { return null; }
189
+ })());
190
+ const envTarget = settings?.env?.[NATIVE_AGENT_TRANSPORT_ENV_KEYS[alias]];
191
+ if (typeof envTarget === 'string' && envTarget.trim()) return envTarget.trim();
192
+ const overrides = options.modelOverrides || settings?.modelOverrides;
181
193
  return typeof overrides?.[key] === 'string' ? overrides[key].trim() : '';
182
194
  }
183
195
 
184
196
  export async function guardAgentModel(input, options = {}) {
185
197
  if (!input || typeof input !== 'object') return {};
186
- if (input.hook_event_name !== 'PreToolUse' || input.tool_name !== 'Agent') return {};
198
+ if (input.hook_event_name !== 'PreToolUse' || !['Agent', 'Task'].includes(input.tool_name)) return {};
187
199
  const toolInput = input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
188
200
  const requested = typeof toolInput.model === 'string' ? toolInput.model.trim() : '';
189
201
  const failoverBlock = readFailoverBlock(toolInput);
@@ -214,6 +226,8 @@ export async function guardAgentModel(input, options = {}) {
214
226
  // Inheritance is safe only when the auditable Bizar selection equals the
215
227
  // actual global Claude parent model and is a selected, enabled user pick.
216
228
  if (!requested || requested === 'inherit') {
229
+ const generated = typeof toolInput.subagent_type === 'string' && [...userPicks].some((id) => toolInput.subagent_type === modelAgentName(id));
230
+ if (generated && !requested) return {};
217
231
  const configured = configuredContext;
218
232
  const parent = readConfiguredParentModel(options);
219
233
  if (!configured || configured !== parent || !userPicks.has(configured)) {
@@ -234,6 +248,13 @@ export async function guardAgentModel(input, options = {}) {
234
248
  return {};
235
249
  }
236
250
 
251
+ // Claude Code accepts full model IDs for native Agent calls. A model picked
252
+ // through `bizar models` is therefore valid directly; do not collapse every
253
+ // user selection into the four family aliases.
254
+ if (userPicks.has(requested) && !hasFailoverContract) {
255
+ return {};
256
+ }
257
+
237
258
  // F-185 contract: when the orchestrator passes both `routingDecisionId`
238
259
  // and `fallback`, validate the fallback against the userSelected pool
239
260
  // but skip the live-discovery re-probe. The fallback's eligibility was
@@ -116,6 +116,28 @@ function requiresGatewayModelDiscovery(modelIds) {
116
116
  return modelIds.some((id) => !/^(?:claude(?:-|$)|anthropic(?:[./-]|$))/i.test(id));
117
117
  }
118
118
 
119
+ // Bind Claude Code's four family aliases to the operator's selected gateway
120
+ // IDs for compatibility. They never imply an Anthropic provider selection and
121
+ // do not constrain full-ID native Agent dispatch.
122
+ const NATIVE_AGENT_ALIASES = ['sonnet', 'opus', 'haiku', 'fable'];
123
+ const NATIVE_AGENT_OVERRIDE_KEYS = {
124
+ sonnet: 'claude-sonnet-5',
125
+ opus: 'claude-opus-5',
126
+ haiku: 'claude-haiku-4-5-20251001',
127
+ fable: 'claude-fable-5',
128
+ };
129
+ const NATIVE_AGENT_ENV_KEYS = {
130
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
131
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
132
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
133
+ fable: 'ANTHROPIC_DEFAULT_FABLE_MODEL',
134
+ };
135
+
136
+ function nativeAgentAliasTargets(modelIds) {
137
+ if (modelIds.length === 0) return {};
138
+ return Object.fromEntries(NATIVE_AGENT_ALIASES.map((alias, index) => [alias, modelIds[index] || modelIds[0]]));
139
+ }
140
+
119
141
  function readSettingsPath() {
120
142
  return join(resolveClaudeConfigDir(), 'settings.json');
121
143
  }
@@ -217,18 +239,24 @@ function syncOnce() {
217
239
  });
218
240
 
219
241
  const overrideKeys = [
220
- 'claude-fable-5', 'claude-opus-5', 'claude-sonnet-5',
221
- 'claude-haiku-4-5-20251001', 'claude-opus-4-8', 'claude-opus-4-7',
242
+ 'claude-sonnet-5', 'claude-opus-5', 'claude-haiku-4-5-20251001', 'claude-fable-5',
243
+ 'claude-opus-4-8', 'claude-opus-4-7',
222
244
  'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-5-20251101',
223
245
  'claude-sonnet-4-5-20250929', 'claude-opus-4-1-20250805',
224
246
  'claude-opus-4-20250514', 'claude-sonnet-4-20250514',
225
247
  'claude-3-7-sonnet-20250219', 'claude-3-5-haiku-20241022',
226
248
  'claude-3-5-sonnet-20241022',
227
249
  ];
250
+ const aliasTargets = nativeAgentAliasTargets(liveIds);
228
251
  settings.modelPicker = { options };
229
252
  settings.modelOverrides = Object.fromEntries(
230
- overrideKeys.map((key, index) => [key, liveIds[index % liveIds.length]]),
253
+ overrideKeys.map((key, index) => {
254
+ const alias = NATIVE_AGENT_ALIASES.find((candidate) => NATIVE_AGENT_OVERRIDE_KEYS[candidate] === key);
255
+ return [key, alias ? aliasTargets[alias] : liveIds[index % liveIds.length]];
256
+ }),
231
257
  );
258
+ settings.env = { ...(settings.env || {}) };
259
+ for (const alias of NATIVE_AGENT_ALIASES) settings.env[NATIVE_AGENT_ENV_KEYS[alias]] = aliasTargets[alias];
232
260
  if (requiresGatewayModelDiscovery(liveIds)) {
233
261
  settings.env = {
234
262
  ...(settings.env || {}),
@@ -22,12 +22,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
22
22
  : ''
23
23
  const routeModel = (risk) => {
24
24
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
25
- return typeof candidate === 'string' ? candidate.trim() : ''
25
+ if (typeof candidate !== 'string') return ''
26
+ const selected = candidate.trim()
27
+ return selected
26
28
  }
27
- if (!routeModel('medium') || !routeModel('high')) {
29
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
30
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
28
31
  return {
29
32
  status: 'blocked',
30
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
33
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
31
34
  }
32
35
  }
33
36
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -35,7 +38,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
35
38
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
36
39
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
37
40
  const agentOptions = {
38
- model: routeModel(opts.risk || 'medium'),
41
+ subagent_type: routeAgentType(opts.risk || 'medium'),
39
42
  effort: opts.risk === 'high' ? 'high' : 'medium',
40
43
  }
41
44
  if (opts.schema) agentOptions.schema = opts.schema
@@ -21,12 +21,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
21
21
  : ''
22
22
  const routeModel = (risk) => {
23
23
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
24
- return typeof candidate === 'string' ? candidate.trim() : ''
24
+ if (typeof candidate !== 'string') return ''
25
+ const selected = candidate.trim()
26
+ return selected
25
27
  }
26
- if (!routeModel('medium') || !routeModel('high')) {
28
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
29
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
27
30
  return {
28
31
  status: 'blocked',
29
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
32
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
30
33
  }
31
34
  }
32
35
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -34,7 +37,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
34
37
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
35
38
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
36
39
  const agentOptions = {
37
- model: routeModel(opts.risk || 'medium'),
40
+ subagent_type: routeAgentType(opts.risk || 'medium'),
38
41
  effort: opts.risk === 'high' ? 'high' : 'medium',
39
42
  }
40
43
  if (opts.schema) agentOptions.schema = opts.schema
@@ -22,12 +22,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
22
22
  : ''
23
23
  const routeModel = (risk) => {
24
24
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
25
- return typeof candidate === 'string' ? candidate.trim() : ''
25
+ if (typeof candidate !== 'string') return ''
26
+ const selected = candidate.trim()
27
+ return selected
26
28
  }
27
- if (!routeModel('medium') || !routeModel('high')) {
29
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
30
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
28
31
  return {
29
32
  status: 'blocked',
30
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
33
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
31
34
  }
32
35
  }
33
36
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -35,7 +38,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
35
38
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
36
39
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
37
40
  const agentOptions = {
38
- model: routeModel(opts.risk || 'medium'),
41
+ subagent_type: routeAgentType(opts.risk || 'medium'),
39
42
  effort: opts.risk === 'high' ? 'high' : 'medium',
40
43
  }
41
44
  if (opts.schema) agentOptions.schema = opts.schema
@@ -33,6 +33,9 @@ import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, ren
33
33
  import { homedir } from 'node:os';
34
34
  import { dirname, isAbsolute, join, resolve } from 'node:path';
35
35
 
36
+ const MODEL_AGENT_WORDS = { a:'alpha', b:'bravo', c:'charlie', d:'delta', e:'echo', f:'foxtrot', g:'golf', h:'hotel', i:'india', j:'juliet', k:'kilo', l:'lima', m:'mike', n:'november', o:'oscar', p:'papa', q:'quebec', r:'romeo', s:'sierra', t:'tango', u:'uniform', v:'victor', w:'whiskey', x:'xray', y:'yankee', z:'zulu', 0:'zero', 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7:'seven', 8:'eight', 9:'nine', '/':'slash', '.':'dot', '-':'dash', '_':'under' };
37
+ function modelAgentName(modelId) { return `bizar-model-${[...String(modelId || '').toLowerCase()].map((ch) => MODEL_AGENT_WORDS[ch] || 'unknown').join('-')}`; }
38
+
36
39
  const { O_APPEND, O_CREAT, O_WRONLY } = fsConstants;
37
40
 
38
41
  /* ────────────────────────────────────────────────────────────────────────── */
@@ -796,14 +799,12 @@ export function classifyDispatchOutcome(result, error, startMs) {
796
799
  }
797
800
 
798
801
  /**
799
- * Build the augmented agent-call payload. The native Agent model field cannot
800
- * carry selected gateway IDs directly. The guard validates that the raw ID is
801
- * an enabled Bizar selection before the native Agent tool sees it.
802
+ * Build an Agent payload that selects a generated full-ID model definition.
802
803
  */
803
- export function augmentPayload(opts, decision, agentName) {
804
+ export function augmentPayload(opts, decision, agentName, context = {}) {
804
805
  return {
805
806
  ...opts,
806
- model: decision.modelId,
807
+ subagent_type: modelAgentName(decision.modelId),
807
808
  additionalContext: {
808
809
  ...(opts.additionalContext && typeof opts.additionalContext === 'object' ? opts.additionalContext : {}),
809
810
  bizarConfiguredModel: decision.modelId ?? null,
@@ -858,13 +859,14 @@ export async function dispatchAgent(agentFn, agentName, prompt, opts = {}, conte
858
859
  if (!agentName || typeof agentName !== 'string') {
859
860
  throw new TypeError('dispatchAgent requires a non-empty agentName');
860
861
  }
861
- const decision = computeDecision(agentName, prompt, opts, context);
862
+ const ctx = context ?? loadDispatchContext();
863
+ const decision = computeDecision(agentName, prompt, opts, ctx);
862
864
  if (!decision.modelId) {
863
865
  throw new ModelRoutingError(
864
866
  'No enabled configured model is available for this Agent dispatch. Configure a model tier or user selection with `bizar models`; refusing to inherit an unconfigured provider default.',
865
867
  );
866
868
  }
867
- const augmented = augmentPayload(opts, decision, agentName);
869
+ const augmented = augmentPayload(opts, decision, agentName, ctx);
868
870
 
869
871
  // F-191 / IMP-018 audit trail — persist the decision before invoking
870
872
  // the agent. Best-effort: telemetry failures MUST NOT abort the
@@ -20,12 +20,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
20
20
  : ''
21
21
  const routeModel = (risk) => {
22
22
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
23
- return typeof candidate === 'string' ? candidate.trim() : ''
23
+ if (typeof candidate !== 'string') return ''
24
+ const selected = candidate.trim()
25
+ return selected
24
26
  }
25
- if (!routeModel('medium') || !routeModel('high')) {
27
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
28
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
26
29
  return {
27
30
  status: 'blocked',
28
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
31
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
29
32
  }
30
33
  }
31
34
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -33,7 +36,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
33
36
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
34
37
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
35
38
  const agentOptions = {
36
- model: routeModel(opts.risk || 'medium'),
39
+ subagent_type: routeAgentType(opts.risk || 'medium'),
37
40
  effort: opts.risk === 'high' ? 'high' : 'medium',
38
41
  }
39
42
  if (opts.schema) agentOptions.schema = opts.schema
@@ -19,12 +19,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
19
19
  : ''
20
20
  const routeModel = (risk) => {
21
21
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
22
- return typeof candidate === 'string' ? candidate.trim() : ''
22
+ if (typeof candidate !== 'string') return ''
23
+ const selected = candidate.trim()
24
+ return selected
23
25
  }
24
- if (!routeModel('medium') || !routeModel('high')) {
26
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
27
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
25
28
  return {
26
29
  status: 'blocked',
27
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
30
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
28
31
  }
29
32
  }
30
33
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -32,7 +35,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
32
35
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
33
36
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
34
37
  const agentOptions = {
35
- model: routeModel(opts.risk || 'medium'),
38
+ subagent_type: routeAgentType(opts.risk || 'medium'),
36
39
  effort: opts.risk === 'high' ? 'high' : 'medium',
37
40
  }
38
41
  if (opts.schema) agentOptions.schema = opts.schema
@@ -21,12 +21,15 @@ const WORKFLOW_DEFAULT_MODEL = typeof WORKFLOW_INPUT.model === 'string'
21
21
  : ''
22
22
  const routeModel = (risk) => {
23
23
  const candidate = WORKFLOW_ROUTING[risk] || WORKFLOW_ROUTING.default || WORKFLOW_DEFAULT_MODEL
24
- return typeof candidate === 'string' ? candidate.trim() : ''
24
+ if (typeof candidate !== 'string') return ''
25
+ const selected = candidate.trim()
26
+ return selected
25
27
  }
26
- if (!routeModel('medium') || !routeModel('high')) {
28
+ const routeAgentType = (risk) => WORKFLOW_ROUTING.agentTypes?.[routeModel(risk)] || ''
29
+ if (!routeModel('medium') || !routeModel('high') || !routeAgentType('medium') || !routeAgentType('high')) {
27
30
  return {
28
31
  status: 'blocked',
29
- reason: 'No explicit configured Bizar model routing was supplied. Read the global Bizar model router and retry with args.routing; provider defaults are prohibited.',
32
+ reason: 'No generated Bizar model-agent mapping was supplied for workflow routing. Run bizar models and retry; provider defaults are prohibited.',
30
33
  }
31
34
  }
32
35
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -34,7 +37,7 @@ const dispatchAgent = (agentFn, agentName, prompt, opts = {}) => {
34
37
  const sequence = ++WORKFLOW_DISPATCH_SEQUENCE
35
38
  const prefix = `[Bizar dispatch ${sequence}: ${agentName}; role=${opts.role || 'worker'}; phase=${opts.phase || 'work'}; label=${opts.label || agentName}]`
36
39
  const agentOptions = {
37
- model: routeModel(opts.risk || 'medium'),
40
+ subagent_type: routeAgentType(opts.risk || 'medium'),
38
41
  effort: opts.risk === 'high' ? 'high' : 'medium',
39
42
  }
40
43
  if (opts.schema) agentOptions.schema = opts.schema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.23.14",
3
+ "version": "10.23.16",
4
4
  "description": "Autonomous, human-in-the-loop multi-agent harness for Claude Code with guarded workflows, typed SDK primitives, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {