@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 +9 -0
- package/cli/commands/models.mjs +46 -20
- package/cli/commands/worker.mjs +125 -0
- package/config/claude/agents/office-manager.md +24 -17
- package/config/claude/hooks/agent-model-guard.mjs +27 -5
- package/config/claude/hooks/sessionstart-model-sync.mjs +31 -3
- package/config/claude/hooks/worker-suggest.mjs +1 -1
- package/config/workflows/bizar-debug.js +4 -2
- package/config/workflows/bizar-implement.js +4 -2
- package/config/workflows/bizar-research.js +4 -2
- package/config/workflows/lib/dispatch.js +10 -31
- package/config/workflows/ultracode-research.js +4 -2
- package/config/workflows/ultracode-review.js +4 -2
- package/config/workflows/ultracode.js +4 -2
- package/package.json +1 -1
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) {
|
package/cli/commands/models.mjs
CHANGED
|
@@ -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
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
export const
|
|
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
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
.
|
|
1077
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
|
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
|
|
38
|
-
small `args.routing` object whose `default`, `medium`,
|
|
39
|
-
explicit enabled configured
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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'))
|
|
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
|
-
|
|
228
|
-
|
|
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-
|
|
221
|
-
'claude-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
816
|
-
*
|
|
817
|
-
*
|
|
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,
|
|
804
|
+
export function augmentPayload(opts, decision, agentName, context = {}) {
|
|
820
805
|
return {
|
|
821
806
|
...opts,
|
|
822
|
-
model:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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