@polderlabs/bizar 10.23.13 → 10.23.15

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) {
@@ -991,6 +991,7 @@ 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);
994
995
  if (requiresGatewayModelDiscovery(synced)) {
995
996
  settings.env = {
996
997
  ...(settings.env || {}),
@@ -1020,8 +1021,8 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = [],
1020
1021
  syncedIds: synced,
1021
1022
  skippedStale: skipped,
1022
1023
  skippedDisabled,
1024
+ nativeAgentAliases,
1023
1025
  settingsPath: path,
1024
- agentAliases: resolveNativeAgentAliases(settings.modelOverrides),
1025
1026
  };
1026
1027
  }
1027
1028
 
@@ -1043,9 +1044,8 @@ export function configuredEnabledModels(router) {
1043
1044
  }
1044
1045
 
1045
1046
  export const CLAUDE_MODEL_OVERRIDE_KEYS = Object.freeze([
1046
- 'claude-fable-5',
1047
- 'claude-opus-5',
1048
1047
  'claude-sonnet-5',
1048
+ 'claude-opus-5',
1049
1049
  'claude-haiku-4-5-20251001',
1050
1050
  'claude-opus-4-8',
1051
1051
  'claude-opus-4-7',
@@ -1061,20 +1061,45 @@ export const CLAUDE_MODEL_OVERRIDE_KEYS = Object.freeze([
1061
1061
  'claude-3-5-sonnet-20241022',
1062
1062
  ]);
1063
1063
 
1064
- // These are Claude Code transport aliases, not model choices. Gateway model
1065
- // IDs remain entirely operator-selected in the global Bizar router. Do not
1066
- // use `fable`: its Claude-branded label is misleading for opted-out users.
1067
- export const NATIVE_AGENT_TRANSPORT_KEYS = Object.freeze({
1064
+ // Claude Code's native Agent tool accepts these transport labels in the
1065
+ // installed runtime. They are labels only: Bizar binds them to the operator's
1066
+ // gateway IDs below; they never select an Anthropic provider by themselves.
1067
+ export const NATIVE_AGENT_ALIASES = Object.freeze(['sonnet', 'opus', 'haiku', 'fable']);
1068
+
1069
+ const NATIVE_AGENT_OVERRIDE_KEYS = Object.freeze({
1068
1070
  sonnet: 'claude-sonnet-5',
1069
1071
  opus: 'claude-opus-5',
1070
1072
  haiku: 'claude-haiku-4-5-20251001',
1073
+ fable: 'claude-fable-5',
1071
1074
  });
1072
1075
 
1073
- export function resolveNativeAgentAliases(modelOverrides) {
1074
- const overrides = modelOverrides && typeof modelOverrides === 'object' ? modelOverrides : {};
1075
- return Object.fromEntries(Object.entries(NATIVE_AGENT_TRANSPORT_KEYS)
1076
- .filter(([, key]) => typeof overrides[key] === 'string' && overrides[key].trim())
1077
- .map(([alias, key]) => [overrides[key].trim(), alias]));
1076
+ export function buildNativeAgentAliasTargets(modelIds) {
1077
+ const unique = [...new Set((Array.isArray(modelIds) ? modelIds : [])
1078
+ .filter((id) => typeof id === 'string' && id.trim())
1079
+ .map((id) => id.trim()))];
1080
+ if (unique.length === 0) return {};
1081
+ return Object.fromEntries(NATIVE_AGENT_ALIASES.map((alias, index) => [
1082
+ alias,
1083
+ unique[index] || unique[0],
1084
+ ]));
1085
+ }
1086
+
1087
+ export function applyNativeAgentAliasTargets(settings, modelIds) {
1088
+ const targets = buildNativeAgentAliasTargets(modelIds);
1089
+ const env = { ...(settings.env || {}) };
1090
+ const envKeys = {
1091
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
1092
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
1093
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
1094
+ fable: 'ANTHROPIC_DEFAULT_FABLE_MODEL',
1095
+ };
1096
+ for (const alias of NATIVE_AGENT_ALIASES) {
1097
+ const envKey = envKeys[alias];
1098
+ if (targets[alias]) env[envKey] = targets[alias];
1099
+ else delete env[envKey];
1100
+ }
1101
+ settings.env = env;
1102
+ return targets;
1078
1103
  }
1079
1104
 
1080
1105
  export function buildClaudeModelOverrides(modelIds) {
@@ -1082,14 +1107,15 @@ export function buildClaudeModelOverrides(modelIds) {
1082
1107
  .filter((id) => typeof id === 'string' && id.trim())
1083
1108
  .map((id) => id.trim()))];
1084
1109
  if (unique.length === 0) return {};
1085
- // Reserve accepted native Agent aliases first so custom gateway IDs have a
1086
- // valid transport without putting the raw ID into Agent.model.
1087
- const transportIndex = new Map(Object.values(NATIVE_AGENT_TRANSPORT_KEYS)
1088
- .map((key, index) => [key, index]));
1089
- return Object.fromEntries(CLAUDE_MODEL_OVERRIDE_KEYS.map((key, index) => [
1090
- key,
1091
- unique[(transportIndex.get(key) ?? index) % unique.length],
1092
- ]));
1110
+ const targets = buildNativeAgentAliasTargets(unique);
1111
+ const nativeEntries = NATIVE_AGENT_ALIASES.map((alias) => [
1112
+ NATIVE_AGENT_OVERRIDE_KEYS[alias],
1113
+ targets[alias],
1114
+ ]);
1115
+ const remaining = CLAUDE_MODEL_OVERRIDE_KEYS
1116
+ .filter((key) => !Object.values(NATIVE_AGENT_OVERRIDE_KEYS).includes(key))
1117
+ .map((key, index) => [key, unique[index % unique.length]]);
1118
+ return Object.fromEntries([...nativeEntries, ...remaining]);
1093
1119
  }
1094
1120
 
1095
1121
  /** 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,20 @@ 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. Native Agent accepts only its transport
42
- aliases (`sonnet`, `opus`, or `haiku`), never a raw gateway ID. Use the alias
43
- that `bizar models` synchronized for the selected ID, and include the selected
44
- ID in `additionalContext.bizarConfiguredModel` for guard verification. If no
45
- configured ID exists, stop and ask the operator to run `bizar models`.
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
+ Pass the chosen full gateway ID directly in every native Agent `model` field;
42
+ current Claude Code supports full model IDs there. Include that same ID in
43
+ `additionalContext.bizarConfiguredModel` for audit telemetry. Never use
44
+ `inherit` or an unconfigured provider default. `bizar models` additionally
45
+ maintains `sonnet`, `opus`, `haiku`, and `fable` aliases as compatibility
46
+ shortcuts, but aliases must not limit dispatch to four selected models. The
47
+ exact-model `bizar worker` command remains available where a separate top-level
48
+ Claude process is useful, not as a fallback for normal native dispatch. If no
49
+ configured model exists, stop and ask the operator to run `bizar models`; never
50
+ omit model selection or cycle providers.
46
51
 
47
52
  Invoke the selected workflow by `name` first. If Claude reports that the Bizar
48
53
  name is unavailable, resolve the active Claude config directory and retry once
@@ -55,14 +60,16 @@ implementation around a broken workflow installation.
55
60
 
56
61
  ## Models
57
62
 
58
- For every Agent call, select the cheapest sufficient enabled configured model
63
+ For every dispatch, select the cheapest sufficient enabled configured model
59
64
  from the global Bizar router. User-selected models take precedence over tier
60
- candidates; `disabledProviders` excludes both. Pass only the synchronized
61
- native transport alias, never a raw gateway ID; the guard verifies its mapping
62
- to the selected custom ID. If no enabled configured candidate exists, stop
63
- with the configuration error. Never let Claude choose an unconfigured default
64
- or retry by cycling aliases, providers, or tiers. A single configured transport
65
- failover is allowed only when the router explicitly supplies it.
65
+ candidates; `disabledProviders` excludes both. Native aliases are transport
66
+ labels bound by `bizar models`, not Anthropic selections. Use the full selected
67
+ model ID directly for native Agents and teams; every enabled selection is
68
+ eligible. Use `bizar worker` only when a separately launched process worktree
69
+ is useful. If no enabled configured candidate exists, stop with the
70
+ configuration error. Never let Claude choose an unconfigured default, inherit
71
+ the session model, use an unmapped alias,
72
+ or retry by cycling models, providers, or tiers.
66
73
 
67
74
  ## Worktree Discipline and integration
68
75
 
@@ -169,15 +169,26 @@ const NATIVE_AGENT_TRANSPORT_KEYS = Object.freeze({
169
169
  sonnet: 'claude-sonnet-5',
170
170
  opus: 'claude-opus-5',
171
171
  haiku: 'claude-haiku-4-5-20251001',
172
+ fable: 'claude-fable-5',
173
+ });
174
+
175
+ const NATIVE_AGENT_TRANSPORT_ENV_KEYS = Object.freeze({
176
+ sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
177
+ opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
178
+ haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
179
+ fable: 'ANTHROPIC_DEFAULT_FABLE_MODEL',
172
180
  });
173
181
 
174
182
  function readTransportTarget(alias, options = {}) {
175
183
  const key = NATIVE_AGENT_TRANSPORT_KEYS[alias];
176
184
  if (!key) return '';
177
- const overrides = options.modelOverrides || (() => {
185
+ const settings = options.settings || (options.modelOverrides ? null : (() => {
178
186
  const settingsPath = options.settingsPath || join(resolveClaudeConfigDir(), 'settings.json');
179
- try { return JSON.parse(readFileSync(settingsPath, 'utf8'))?.modelOverrides; } catch { return null; }
180
- })();
187
+ try { return JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { return null; }
188
+ })());
189
+ const envTarget = settings?.env?.[NATIVE_AGENT_TRANSPORT_ENV_KEYS[alias]];
190
+ if (typeof envTarget === 'string' && envTarget.trim()) return envTarget.trim();
191
+ const overrides = options.modelOverrides || settings?.modelOverrides;
181
192
  return typeof overrides?.[key] === 'string' ? overrides[key].trim() : '';
182
193
  }
183
194
 
@@ -224,12 +235,23 @@ export async function guardAgentModel(input, options = {}) {
224
235
 
225
236
  const transportTarget = readTransportTarget(requested, options);
226
237
  if (transportTarget) {
227
- if (configuredContext !== transportTarget || !userPicks.has(transportTarget)) {
228
- return deny(`Bizar Agent dispatch blocked: native alias ${requested} does not map to the audited enabled Bizar selection. Re-run \`bizar models\` and restart Claude Code.`);
238
+ // `modelOverrides` is the native Agent transport contract: Claude Code
239
+ // invokes this alias with the mapped gateway ID. The optional context is
240
+ // useful for telemetry, but must not make a valid configured alias fail
241
+ // when Mike's prompt carries a different (also selected) planning pick.
242
+ if (!userPicks.has(transportTarget)) {
243
+ return deny(`Bizar Agent dispatch blocked: native alias ${requested} does not map to an enabled Bizar selection. Re-run \`bizar models\` and restart Claude Code.`);
229
244
  }
230
245
  return {};
231
246
  }
232
247
 
248
+ // Claude Code accepts full model IDs for native Agent calls. A model picked
249
+ // through `bizar models` is therefore valid directly; do not collapse every
250
+ // user selection into the four family aliases.
251
+ if (userPicks.has(requested) && !hasFailoverContract) {
252
+ return {};
253
+ }
254
+
233
255
  // F-185 contract: when the orchestrator passes both `routingDecisionId`
234
256
  // and `fallback`, validate the fallback against the userSelected pool
235
257
  // 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 || {}),
@@ -60,7 +60,7 @@ const ROUTE_POLICY = [
60
60
  'Adaptive Bizar routing policy:',
61
61
  '- If this is the primary session, you ARE @mike. First do only bounded read-only orientation. Then ask one concise clarification question describing the inferred outcome, the material choice/risk, and your proposed coordination mode. Wait for the answer before edits, branches, tests, or editor dispatch. If the user explicitly waives questions, continue autonomously.',
62
62
  '- After clarification, choose the lightest coordination mode: a direct tiny edit, one isolated Agent for a bounded change, a native Bizar Workflow for repeatable phased work, parallel Agents for disjoint scopes, or an Agent team only when 3+ sustained roles need cross-talk. Do not force a workflow or team when it adds no value.',
63
- '- Every Agent or team member receives an enabled configured Bizar selection. Native Agent calls use only the synchronized sonnet/opus/haiku transport alias plus additionalContext.bizarConfiguredModel; the guard proves that alias maps to the selected custom gateway ID. Never pass raw gateway IDs or fall back to a provider default. Every editing worker uses call-level `isolation: "worktree"`. For genuinely disjoint writable scopes, dispatch concurrently; otherwise use one owner.',
63
+ '- Every Agent or team member receives one exact raw model ID from the enabled `bizar models` user selection. Pass that ID directly as `model`; never substitute a Claude family alias, provider default, or inherited session model. The guard allows only IDs in the global user-selected pool. If a selected custom model is denied, inspect `~/.claude/model-router.json`; do not retry with another model. Every editing worker uses call-level `isolation: "worktree"`. For genuinely disjoint writable scopes, dispatch concurrently; otherwise use one owner.',
64
64
  '- Consume terminal agent results, merge queued worktrees with bizar worktree-merge, and run integration checks in the primary session. A subagent may not recursively dispatch itself.',
65
65
  '- Do NOT execute any tool you do not have. If a tool you need is missing from your tools list, dispatch to a subagent that has it — do not pretend you have it.',
66
66
  '- If you are already running as a Bizar custom agent, follow your assigned role and do not recursively dispatch yourself.',
@@ -22,12 +22,14 @@ 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
29
  if (!routeModel('medium') || !routeModel('high')) {
28
30
  return {
29
31
  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.',
32
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
31
33
  }
32
34
  }
33
35
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -21,12 +21,14 @@ 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
28
  if (!routeModel('medium') || !routeModel('high')) {
27
29
  return {
28
30
  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.',
31
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
30
32
  }
31
33
  }
32
34
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -22,12 +22,14 @@ 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
29
  if (!routeModel('medium') || !routeModel('high')) {
28
30
  return {
29
31
  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.',
32
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
31
33
  }
32
34
  }
33
35
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -387,24 +387,9 @@ function defaultConfigPaths({ cwd = process.cwd(), env = process.env } = {}) {
387
387
  health: join(home, 'health.json'),
388
388
  budget: join(home, 'budget.json'),
389
389
  userSelected: join(home, 'userSelected.json'),
390
- claudeSettings: join(claudeDir, 'settings.json'),
391
390
  };
392
391
  }
393
392
 
394
- const NATIVE_AGENT_TRANSPORT_KEYS = Object.freeze({
395
- sonnet: 'claude-sonnet-5',
396
- opus: 'claude-opus-5',
397
- haiku: 'claude-haiku-4-5-20251001',
398
- });
399
-
400
- function loadAgentTransportAliases(settingsPath) {
401
- const overrides = readJson(settingsPath)?.modelOverrides;
402
- if (!overrides || typeof overrides !== 'object') return {};
403
- return Object.fromEntries(Object.entries(NATIVE_AGENT_TRANSPORT_KEYS)
404
- .filter(([, key]) => typeof overrides[key] === 'string' && overrides[key].trim())
405
- .map(([alias, key]) => [overrides[key].trim(), alias]));
406
- }
407
-
408
393
  function readJson(path) {
409
394
  try {
410
395
  if (!existsSync(path)) return undefined;
@@ -479,8 +464,7 @@ export function loadDispatchContext({ cwd = process.cwd(), env = process.env } =
479
464
  const healthRaw = readJson(paths.health) ?? {};
480
465
  const health = (healthRaw && typeof healthRaw === 'object' && healthRaw.models) || {};
481
466
  const activeSessionModel = env.BIZAR_ACTIVE_SESSION_MODEL ?? loadActiveSessionModel();
482
- const agentAliases = loadAgentTransportAliases(paths.claudeSettings);
483
- return { selectedProfiles, staticProfiles, activeSessionModel, budget, health, agentAliases, evidenceDir: resolveEvidenceDir() };
467
+ return { selectedProfiles, staticProfiles, activeSessionModel, budget, health, evidenceDir: resolveEvidenceDir() };
484
468
  }
485
469
 
486
470
  function resolveEvidenceDir({ cwd = process.cwd(), env = process.env } = {}) {
@@ -812,14 +796,15 @@ export function classifyDispatchOutcome(result, error, startMs) {
812
796
  }
813
797
 
814
798
  /**
815
- * Build the augmented agent-call payload. The native Agent model field cannot
816
- * carry arbitrary gateway IDs, so the selected Bizar ID is audit context and
817
- * the guard permits inheritance only from an identical configured parent.
799
+ * Build the augmented native-Agent payload. Claude Code supports an explicit
800
+ * full model ID in an Agent invocation; use the selected gateway ID directly
801
+ * so every configured selection is usable by native agents and teams. The
802
+ * transport aliases remain a compatibility option for callers that need them.
818
803
  */
819
- export function augmentPayload(opts, decision, agentName, transportAlias) {
804
+ export function augmentPayload(opts, decision, agentName, context = {}) {
820
805
  return {
821
806
  ...opts,
822
- model: transportAlias,
807
+ model: decision.modelId,
823
808
  additionalContext: {
824
809
  ...(opts.additionalContext && typeof opts.additionalContext === 'object' ? opts.additionalContext : {}),
825
810
  bizarConfiguredModel: decision.modelId ?? null,
@@ -874,20 +859,14 @@ export async function dispatchAgent(agentFn, agentName, prompt, opts = {}, conte
874
859
  if (!agentName || typeof agentName !== 'string') {
875
860
  throw new TypeError('dispatchAgent requires a non-empty agentName');
876
861
  }
877
- const decision = computeDecision(agentName, prompt, opts, context);
862
+ const ctx = context ?? loadDispatchContext();
863
+ const decision = computeDecision(agentName, prompt, opts, ctx);
878
864
  if (!decision.modelId) {
879
865
  throw new ModelRoutingError(
880
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.',
881
867
  );
882
868
  }
883
- const ctx = context ?? loadDispatchContext();
884
- const transportAlias = ctx.agentAliases?.[decision.modelId];
885
- if (!transportAlias) {
886
- throw new ModelRoutingError(
887
- `No native Agent transport alias maps to ${decision.modelId}. Re-run \`bizar models\` to synchronize global Claude settings, then restart Claude Code.`,
888
- );
889
- }
890
- const augmented = augmentPayload(opts, decision, agentName, transportAlias);
869
+ const augmented = augmentPayload(opts, decision, agentName, ctx);
891
870
 
892
871
  // F-191 / IMP-018 audit trail — persist the decision before invoking
893
872
  // the agent. Best-effort: telemetry failures MUST NOT abort the
@@ -20,12 +20,14 @@ 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
27
  if (!routeModel('medium') || !routeModel('high')) {
26
28
  return {
27
29
  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.',
30
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
29
31
  }
30
32
  }
31
33
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -19,12 +19,14 @@ 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
26
  if (!routeModel('medium') || !routeModel('high')) {
25
27
  return {
26
28
  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.',
29
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
28
30
  }
29
31
  }
30
32
  let WORKFLOW_DISPATCH_SEQUENCE = 0
@@ -21,12 +21,14 @@ 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
28
  if (!routeModel('medium') || !routeModel('high')) {
27
29
  return {
28
30
  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.',
31
+ reason: 'No explicit enabled Bizar model was supplied for workflow routing. Read the global model router and retry with args.routing; provider defaults are prohibited.',
30
32
  }
31
33
  }
32
34
  let WORKFLOW_DISPATCH_SEQUENCE = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.23.13",
3
+ "version": "10.23.15",
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": {