@polderlabs/bizar 10.23.8 → 10.23.10

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/AGENTS.md CHANGED
@@ -58,21 +58,18 @@ still deny prohibited actions and escalate externally visible or irreversible
58
58
  actions with `permissionDecision: "ask"`; that escalation list is the
59
59
  authoritative floor, not a starting point.
60
60
 
61
- Native dynamic workflows under `config/workflows/` and `~/.claude/workflows/`
62
- are the required dispatch mechanism for every primary request except an
63
- unmistakably tiny single-target copy/style/format edit with no behavior or test
64
- change. Mike may execute that narrow exception directly. All other work enters
65
- the matching research / implement / debug / review workflow and uses at least
66
- one explicitly modeled editing subagent with call-level worktree isolation.
67
- Do not add unnecessary phases or duplicate workers. For work that needs 3+
68
- long-lived workers with bounded cross-talk,
69
- Mike invokes a workflow that fans out as a native agent team; the team is
61
+ Mike selects the coordination mode after bounded read-only orientation and one
62
+ user clarification checkpoint: direct work only for an unmistakably tiny
63
+ single-target copy/style/format edit; one isolated Agent for a clear bounded
64
+ change; a native workflow for repeatable phased work; parallel Agents for
65
+ disjoint scopes; and an Agent team for 3+ sustained roles that genuinely need
66
+ cross-talk. Do not add unnecessary phases or duplicate workers. The team is
70
67
  host-side state under `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`, and per
71
- Anthropic's docs `team_name` is deprecated and ignored. When two or more
72
- subtasks have non-overlapping writable scopes and no data dependency, the
73
- orchestrator MUST dispatch them concurrently through `parallel([...])`, each
74
- with call-level `isolation: "worktree"`. Sequential dispatch is reserved for
75
- dependent phases and integration; never create artificial parallel work.
68
+ Anthropic's docs `team_name` is deprecated and ignored. Every editing dispatch
69
+ uses an explicit configured model and call-level `isolation: "worktree"`.
70
+ When two or more subtasks have non-overlapping writable scopes and no data
71
+ dependency, dispatch them concurrently; serialize only dependencies and
72
+ integration.
76
73
 
77
74
  Agent roles are model-agnostic. Mike selects the cheapest sufficient enabled
78
75
  configured-tier model for each dispatch, with explicit user picks taking
@@ -136,11 +133,13 @@ The autonomy and approval policy above governs this execution model. The project
136
133
  Every non-empty primary request enters Bizar through `office-manager` (`@mike`).
137
134
  The installer sets Claude Code's global `agent` setting to Mike's frontmatter
138
135
  name (`mike`),
139
- and a session-scoped routing guard requires a successful native Workflow before
140
- substantive primary-session mutation. Read-only inspection remains available.
141
- Mike directly executes only the tiny edit exception above. Every other request
142
- must invoke a native Bizar workflow before mutation; the workflow dispatches
143
- worktree-isolated subagents while Mike owns integration and final verification.
136
+ and the routing hook supplies the adaptive coordination policy. For non-tiny
137
+ work, Mike first gathers only bounded read-only context, asks one concise
138
+ clarification question that names the inferred outcome and proposed mode, then
139
+ continues autonomously after the answer. A native workflow is one available
140
+ mode, not a universal gate; Mike may select an isolated Agent, parallel Agents,
141
+ or an Agent team when that better fits the work. Mike owns integration and
142
+ final verification.
144
143
  A Bizar custom agent already executing its assigned role does not recursively
145
144
  dispatch itself.
146
145
 
@@ -152,8 +151,8 @@ relevant page. Guess-and-try integration work is prohibited. When official
152
151
  documentation is unavailable or ambiguous, inspect authoritative source code
153
152
  and report the evidence gap.
154
153
 
155
- For workflow-routed requests, `office-manager` uses only the phases that reduce
156
- a known risk; only the tiny edit exception skips this pipeline:
154
+ For a workflow or team, `office-manager` uses only phases and members that
155
+ reduce a known risk:
157
156
 
158
157
  1. Research: `greg` (`research-analyst.md`) plus an implementation-context specialist.
159
158
  2. Plan: `planner` drafts; `qa-reviewer` challenges assumptions and test shape.
@@ -716,9 +716,8 @@ export function classifyKind(modelId) {
716
716
  // in-code hardcoded list — adding or removing a blocked provider is a
717
717
  // single JSON edit.
718
718
  //
719
- // Dual-path read: the Bizar path (`~/.config/bizar/config/claude/model-router.json`)
720
- // WINS when both exist, including an explicit `[]` (operators may pin
721
- // "no providers disabled" without deleting the legacy mirror). Whitespace
719
+ // Global-only read: the Bizar path (`~/.config/bizar/config/claude/model-router.json`)
720
+ // is the sole operator policy source. Whitespace
722
721
  // trim + lowercase normalization happens at read time so operators may
723
722
  // write `" Anthropic "` in JSON and still match `anthropic/...` model
724
723
  // ids. The comparison itself is a case-sensitive prefix filter against
@@ -758,44 +757,28 @@ function extractDisabledProviders(router) {
758
757
 
759
758
  /**
760
759
  * Read the operator's `disabledProviders` list from the model-router
761
- * config. Dual-path: the Bizar path wins when present (even with an
762
- * explicit empty array); the legacy `~/.claude/model-router.json` mirror
763
- * is the fallback. Both paths are normalized (trim + lowercase) at read.
760
+ * config. The Bizar global path is the only source. It is normalized (trim +
761
+ * lowercase) at read.
764
762
  *
765
763
  * Pure function over the filesystem; returns an empty array when neither
766
- * file exists, when both files lack the key, or when both reads fail.
764
+ * file exists, lacks the key, or cannot be read.
767
765
  *
768
- * @param {{ routerPath?: string, legacyPath?: string }} [opts]
766
+ * @param {{ routerPath?: string }} [opts]
769
767
  * - `routerPath` defaults to the Bizar home path
770
768
  * (`~/.config/bizar/config/claude/model-router.json`).
771
- * - `legacyPath` defaults to the Claude Code mirror
772
- * (`~/.claude/model-router.json`).
773
769
  * @returns {string[]} normalized disabled-provider prefixes
774
770
  */
775
- export function readDisabledProviders({ routerPath, legacyPath } = {}) {
771
+ export function readDisabledProviders({ routerPath } = {}) {
776
772
  const bizarPath = routerPath || resolveGlobalModelRouter();
777
- const fallPath = legacyPath || join(resolveClaudeConfigDir(), 'model-router.json');
778
773
  if (existsSync(bizarPath)) {
779
774
  try {
780
775
  const parsed = JSON.parse(readFileSync(bizarPath, 'utf8'));
781
776
  const extracted = extractDisabledProviders(parsed);
782
- // Bizar path exists its `disabledProviders` is authoritative even
783
- // when explicitly `[]` (operators may pin "no providers disabled"
784
- // without deleting the legacy mirror). Missing key still falls back.
777
+ // An explicit empty list is a valid global opt-out policy.
785
778
  if (Array.isArray(parsed && typeof parsed === 'object' ? parsed.disabledProviders : undefined)) {
786
779
  return extracted;
787
780
  }
788
- } catch {
789
- // Corrupt Bizar file — fall through to the legacy mirror.
790
- }
791
- }
792
- if (existsSync(fallPath)) {
793
- try {
794
- const parsed = JSON.parse(readFileSync(fallPath, 'utf8'));
795
- return extractDisabledProviders(parsed);
796
- } catch {
797
- return [];
798
- }
781
+ } catch { return []; }
799
782
  }
800
783
  return [];
801
784
  }
@@ -5,7 +5,7 @@
5
5
  * `bizar tier` — automatic task-based model selection.
6
6
  *
7
7
  * Replays the same tier resolution the @mike orchestrator uses:
8
- * 1. Read the role default from ~/.claude/model-router.json.
8
+ * 1. Read the role default from the global Bizar model router.
9
9
  * 2. Adjust the tier from current task risk and complexity.
10
10
  * 3. Print ordered candidates; dispatch uses a candidate only when live
11
11
  * discovery proves it, otherwise Agent omits model and inherits the session.
@@ -42,13 +42,11 @@ const CLAUDE_DIR = resolveClaudeConfigDir();
42
42
  const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
43
43
  // 10.22.0 / Phase 4 spirit-of-constraint: the install model id comes
44
44
  // from the operator's `userSelected.models[0]`, not a hardcoded literal.
45
- // Dual-path read matches `cli/commands/models.mjs#readDisabledProviders`:
46
- // the Bizar path wins when present; the Claude Code mirror is fallback.
45
+ // The Bizar global router is the sole model-policy source.
47
46
  const BIZAR_ROUTER_PATH = resolveGlobalModelRouter();
48
- const LEGACY_ROUTER_PATH = join(CLAUDE_DIR, 'model-router.json');
49
47
 
50
48
  function readConfiguredModels() {
51
- for (const path of [BIZAR_ROUTER_PATH, LEGACY_ROUTER_PATH]) {
49
+ for (const path of [BIZAR_ROUTER_PATH]) {
52
50
  if (!existsSync(path)) continue;
53
51
  try {
54
52
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
@@ -58,8 +56,7 @@ function readConfiguredModels() {
58
56
  if (list.length > 0) return list;
59
57
  const fallback = configuredFallbackModels(parsed);
60
58
  if (fallback.length > 0) return fallback;
61
- // Bizar path exists with an explicit empty `userSelected.models` —
62
- // honour that intent (do NOT fall through to the legacy mirror).
59
+ // An explicit empty selection is an intentional global policy.
63
60
  if (path === BIZAR_ROUTER_PATH && parsed && typeof parsed === 'object'
64
61
  && Array.isArray(parsed.userSelected?.models)) {
65
62
  return [];
package/cli/provision.mjs CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  import { homedir } from 'node:os';
32
32
  import { dirname, join, resolve, sep } from 'node:path';
33
33
  import { fileURLToPath } from 'node:url';
34
- import { resolveBizarHome } from './config-paths.mjs';
34
+ import { resolveBizarHome, resolveGlobalModelRouter } from './config-paths.mjs';
35
35
  import {
36
36
  buildClaudeModelOverrides,
37
37
  configuredEnabledModels,
@@ -465,9 +465,20 @@ export async function syncAgentFiles({ dryRun = false, force = false } = {}) {
465
465
  return { ok: true, message: `${copied} agent(s) synced (${skipped} kept)${sharedCopied ? `, ${sharedCopied} _shared/*.md` : ''}${tail}`, copied, skipped, pruned };
466
466
  }
467
467
 
468
- // The dynamic model router lives at config/claude/model-router.json and is
469
- // copied to user-level `$CLAUDE_CONFIG_DIR/model-router.json` so Mike and the
470
- // SDK can select tiers without a repository cwd dependency.
468
+ // The dynamic model router is operator-owned state. It is created only under
469
+ // Bizar's global config root; a repository can never supply a runtime model.
470
+
471
+ export const EMPTY_GLOBAL_MODEL_ROUTER = {
472
+ $schema: 'https://bizar.dev/schema/model-router.v3.json',
473
+ version: '13.0.0',
474
+ endpoint: null,
475
+ disabledProviders: ['anthropic'],
476
+ userSelected: { models: [], lastUpdated: null, source: 'default-empty', tierHints: {} },
477
+ gateway: { required: false, endpoint: null, availabilityProbe: '/models', discoveryTimeoutMs: 3000, unavailableBehavior: 'configured-tier-fallback' },
478
+ tiers: Object.fromEntries(['premium', 'high', 'mid-design', 'default', 'mid', 'budget'].map((name) => [name, { models: [] }])),
479
+ roleDefaults: { mike: 'premium', paul: 'premium', carl: 'premium', karen: 'high', linda: 'high', ria: 'mid-design', greg: 'default', steve: 'default', oscar: 'mid', todd: 'mid', susan: 'mid', pam: 'budget', brenda: 'budget', janet: 'budget', kevin: 'budget', brad: 'mid-design' },
480
+ policies: { mainOrchestrator: 'mike', selectionOwner: 'orchestrator', dispatchModelOverride: 'required-configured-candidate', unknownAgent: 'configured-tier-fallback', discoveryFailure: 'configured-tier-fallback', unavailableModel: 'configured-tier-fallback', silentProviderSubstitution: false, retryModelAliases: false, snapshotDecisions: true, maxDispatchModelAttempts: 1 },
481
+ };
471
482
 
472
483
  export function isBizarManagedModelRouter(value) {
473
484
  return Boolean(
@@ -479,16 +490,14 @@ export function isBizarManagedModelRouter(value) {
479
490
  }
480
491
 
481
492
  export async function syncModelRouter({ dryRun = false, force = false } = {}) {
482
- const src = join(REPO_ROOT, 'config', 'claude', 'model-router.json');
483
- const dest = join(CLAUDE_DIR, 'model-router.json');
484
- if (!existsSync(src)) return { ok: true, message: `no model-router at ${src}` };
485
- if (existsSync(dest) && !force && !isBizarManagedModelRouter(readJsonSafe(dest, null))) {
486
- return { ok: true, message: `${dest} is user-managed — pass --force to overwrite`, preserved: true };
493
+ const dest = resolveGlobalModelRouter();
494
+ if (existsSync(dest)) {
495
+ return { ok: true, message: `${dest} is operator-managed preserved`, preserved: true };
487
496
  }
488
- if (dryRun) return { ok: true, message: `[dry-run] would copy ${src} ${dest}` };
489
- ensureDir(CLAUDE_DIR);
490
- copyFileSync(src, dest);
491
- return { ok: true, message: `model-router.json → ${dest}`, path: dest };
497
+ if (dryRun) return { ok: true, message: `[dry-run] would create global model router at ${dest}` };
498
+ ensureDir(dirname(dest));
499
+ writeFileSync(dest, `${JSON.stringify(EMPTY_GLOBAL_MODEL_ROUTER, null, 2)}\n`);
500
+ return { ok: true, message: `global model-router.json → ${dest}`, path: dest };
492
501
  }
493
502
 
494
503
  export async function syncSkillFiles({ dryRun = false, force = false } = {}) {
@@ -949,16 +958,12 @@ export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
949
958
  // wins; otherwise the first enabled configured tier candidate is used.
950
959
  // This prevents Claude Code from falling through to an unconfigured
951
960
  // provider default when `userSelected.models` is intentionally empty.
952
- // Dual-path read matches
953
- // `cli/commands/models.mjs#readDisabledProviders` and
954
- // `cli/commands/upgrade-defaults.mjs#readUserSelectedModels`.
955
- const bizarRouterPath = process.env.BIZAR_MODEL_ROUTER_CONFIG?.trim()
956
- || join(BIZAR_HOME(), 'config', 'claude', 'model-router.json');
957
- const legacyRouterPath = join(CLAUDE_DIR, 'model-router.json');
961
+ // The Bizar global router is the sole source of model policy and picks.
962
+ const bizarRouterPath = resolveGlobalModelRouter();
958
963
  let installModel;
959
964
  let installContextTokens;
960
965
  let installModels = [];
961
- for (const p of [bizarRouterPath, legacyRouterPath]) {
966
+ for (const p of [bizarRouterPath]) {
962
967
  if (!existsSync(p)) continue;
963
968
  const parsed = readJsonSafe(p, null);
964
969
  if (!parsed || typeof parsed !== 'object') continue;
@@ -966,8 +971,6 @@ export function writeClaudeSettings({ dryRun = false, force = false } = {}) {
966
971
  installModels = configuredEnabledModels(parsed);
967
972
  installContextTokens = installModel ? configuredModelContextTokens(parsed, installModel) : undefined;
968
973
  if (installModel) break;
969
- // The Bizar path is authoritative even when it has no enabled candidates.
970
- if (p === bizarRouterPath) break;
971
974
  }
972
975
  if (installModel) {
973
976
  bizarSettings.model = installModel;
@@ -1177,16 +1180,16 @@ export async function runProvision(opts = {}) {
1177
1180
  await runStep('Installing git hooks', () => installGitHooks({ dryRun }));
1178
1181
  await runStep('Building SDK', () => buildSdk({ dryRun }));
1179
1182
 
1183
+ section('Ensuring global model-router.json');
1184
+ const routerStep = await syncModelRouter({ dryRun, force });
1185
+ if (routerStep.ok) logOk(routerStep.message); else logErr(routerStep.message);
1186
+ stepResults.push({ label: 'model-router', ...routerStep });
1187
+
1180
1188
  section('Writing settings.json');
1181
1189
  const settingsStep = writeClaudeSettings({ dryRun, force });
1182
1190
  if (settingsStep.ok) logOk(settingsStep.message); else logErr(settingsStep.message);
1183
1191
  stepResults.push({ label: 'settings.json', ...settingsStep });
1184
1192
 
1185
- section('Syncing model-router.json');
1186
- const routerStep = await syncModelRouter({ dryRun, force });
1187
- if (routerStep.ok) logOk(routerStep.message); else logErr(routerStep.message);
1188
- stepResults.push({ label: 'model-router', ...routerStep });
1189
-
1190
1193
  section('MCP server (bizar)');
1191
1194
  const mcpStep = setupMcpServer({ dryRun });
1192
1195
  if (mcpStep.ok) logOk(mcpStep.message); else logWarn(mcpStep.message);
@@ -1221,7 +1224,7 @@ export async function runProvision(opts = {}) {
1221
1224
  // back to a clear hint to run `bizar models`.
1222
1225
  let premiumPick = null;
1223
1226
  try {
1224
- const routerPath = join(BIZAR_HOME(), 'config', 'claude', 'model-router.json');
1227
+ const routerPath = resolveGlobalModelRouter();
1225
1228
  const router = JSON.parse(readFileSync(routerPath, 'utf8'));
1226
1229
  const picks = Array.isArray(router?.userSelected?.tierHints?.premium)
1227
1230
  ? router.userSelected.tierHints.premium.filter((id) => typeof id === 'string' && id)
@@ -8,11 +8,12 @@
8
8
  */
9
9
  import { createHash } from 'node:crypto';
10
10
  import { readFileSync } from 'node:fs';
11
- import { resolve } from 'node:path';
12
- import { fileURLToPath } from 'node:url';
11
+ import { resolveGlobalModelRouter } from '../../cli/config-paths.mjs';
13
12
 
14
- const ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
15
- export const DEFAULT_MODEL_ROUTER_PATH = resolve(ROOT, 'config', 'claude', 'model-router.json');
13
+ /** Resolve the single operator-owned router at call time, never from a repo. */
14
+ export function defaultModelRouterPath() {
15
+ return resolveGlobalModelRouter();
16
+ }
16
17
 
17
18
  function fail(code, message) {
18
19
  const error = new Error(message);
@@ -39,9 +40,6 @@ function assertDynamicRegistry(registry) {
39
40
  if (!registry.tiers || typeof registry.tiers !== 'object' || Object.keys(registry.tiers).length === 0) {
40
41
  fail('MODEL_REGISTRY_INVALID', 'The model router must define at least one dynamic tier.');
41
42
  }
42
- for (const [name, tier] of Object.entries(registry.tiers)) {
43
- if (normalizeModelIds(tier?.models).length === 0) fail('MODEL_REGISTRY_INVALID', `Tier ${name} has no candidate models.`);
44
- }
45
43
  const policies = registry.policies || {};
46
44
  if (policies.selectionOwner !== 'orchestrator') fail('MODEL_POLICY_INVALID', 'The orchestrator must own model selection.');
47
45
  if (!['configured-tier-fallback', 'inherit-session'].includes(policies.discoveryFailure)
@@ -54,7 +52,7 @@ function assertDynamicRegistry(registry) {
54
52
  return registry;
55
53
  }
56
54
 
57
- export function loadModelRouter(path = DEFAULT_MODEL_ROUTER_PATH) {
55
+ export function loadModelRouter(path = defaultModelRouterPath()) {
58
56
  try {
59
57
  return assertDynamicRegistry(JSON.parse(readFileSync(path, 'utf8')));
60
58
  } catch (error) {
@@ -73,7 +71,16 @@ export function resolveDispatchModel({
73
71
  const definition = registry.tiers?.[chosenTier];
74
72
  if (!definition) fail('UNKNOWN_TIER', `Unknown model tier ${chosenTier}.`);
75
73
  const disabled = new Set(normalizeModelIds(registry.disabledProviders).map((prefix) => prefix.toLowerCase()));
76
- const candidates = normalizeModelIds(definition.models)
74
+ const selected = normalizeModelIds(registry.userSelected?.models)
75
+ .filter((candidate) => ![...disabled].some((prefix) => candidate.toLowerCase().startsWith(prefix)));
76
+ const hints = registry.userSelected?.tierHints && typeof registry.userSelected.tierHints === 'object'
77
+ ? registry.userSelected.tierHints
78
+ : {};
79
+ // The operator's picker is authoritative. A matching tier hint narrows the
80
+ // pool; otherwise all enabled picks remain eligible rather than silently
81
+ // falling back to repository-supplied candidates.
82
+ const hinted = selected.filter((candidate) => hints[candidate] === chosenTier);
83
+ const candidates = (hinted.length > 0 ? hinted : selected.length > 0 ? selected : normalizeModelIds(definition.models))
77
84
  .filter((candidate) => ![...disabled].some((prefix) => candidate.toLowerCase().startsWith(prefix)));
78
85
  if (candidates.length === 0) fail('NO_ENABLED_TIER_MODEL', `Tier ${chosenTier} has no enabled candidate models.`);
79
86
  const available = availableModelIds == null ? null : new Set(normalizeModelIds(availableModelIds));
@@ -69,21 +69,18 @@ still deny prohibited actions and escalate externally visible or irreversible
69
69
  actions with `permissionDecision: "ask"`; that escalation list is the
70
70
  authoritative floor, not a starting point.
71
71
 
72
- Native dynamic workflows under `config/workflows/` and `~/.claude/workflows/`
73
- are the required dispatch mechanism for every primary request except an
74
- unmistakably tiny single-target copy/style/format edit with no behavior or test
75
- change. Mike may execute that narrow exception directly. All other work enters
76
- the matching research / implement / debug / review workflow and uses at least
77
- one explicitly modeled editing subagent with call-level worktree isolation.
78
- Do not add unnecessary phases or duplicate workers. For work that needs 3+
79
- long-lived workers with bounded cross-talk,
80
- Mike invokes a workflow that fans out as a native agent team; the team is
72
+ Mike selects the coordination mode after bounded read-only orientation and one
73
+ user clarification checkpoint: direct work only for an unmistakably tiny
74
+ single-target copy/style/format edit; one isolated Agent for a clear bounded
75
+ change; a native workflow for repeatable phased work; parallel Agents for
76
+ disjoint scopes; and an Agent team for 3+ sustained roles that genuinely need
77
+ cross-talk. Do not add unnecessary phases or duplicate workers. The team is
81
78
  host-side state under `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`, and per
82
- Anthropic's docs `team_name` is deprecated and ignored. When two or more
83
- subtasks have non-overlapping writable scopes and no data dependency, the
84
- orchestrator MUST dispatch them concurrently through `parallel([...])`, each
85
- with call-level `isolation: "worktree"`. Sequential dispatch is reserved for
86
- dependent phases and integration; never create artificial parallel work.
79
+ Anthropic's docs `team_name` is deprecated and ignored. Every editing dispatch
80
+ uses an explicit configured model and call-level `isolation: "worktree"`.
81
+ When two or more subtasks have non-overlapping writable scopes and no data
82
+ dependency, dispatch them concurrently; serialize only dependencies and
83
+ integration.
87
84
 
88
85
  Agent roles are model-agnostic. Mike selects the cheapest sufficient enabled
89
86
  configured-tier model for each dispatch, with explicit user picks taking
@@ -147,11 +144,13 @@ The autonomy and approval policy above governs this execution model. The project
147
144
  Every non-empty primary request enters Bizar through `office-manager` (`@mike`).
148
145
  The installer sets Claude Code's global `agent` setting to Mike's frontmatter
149
146
  name (`mike`),
150
- and a session-scoped routing guard requires a successful native Workflow before
151
- substantive primary-session mutation. Read-only inspection remains available.
152
- Mike directly executes only the tiny edit exception above. Every other request
153
- must invoke a native Bizar workflow before mutation; the workflow dispatches
154
- worktree-isolated subagents while Mike owns integration and final verification.
147
+ and the routing hook supplies the adaptive coordination policy. For non-tiny
148
+ work, Mike first gathers only bounded read-only context, asks one concise
149
+ clarification question that names the inferred outcome and proposed mode, then
150
+ continues autonomously after the answer. A native workflow is one available
151
+ mode, not a universal gate; Mike may select an isolated Agent, parallel Agents,
152
+ or an Agent team when that better fits the work. Mike owns integration and
153
+ final verification.
155
154
  A Bizar custom agent already executing its assigned role does not recursively
156
155
  dispatch itself.
157
156
 
@@ -163,8 +162,8 @@ relevant page. Guess-and-try integration work is prohibited. When official
163
162
  documentation is unavailable or ambiguous, inspect authoritative source code
164
163
  and report the evidence gap.
165
164
 
166
- For workflow-routed requests, `office-manager` uses only the phases that reduce
167
- a known risk; only the tiny edit exception skips this pipeline:
165
+ For a workflow or team, `office-manager` uses only phases and members that
166
+ reduce a known risk:
168
167
 
169
168
  1. Research: `greg` (`research-analyst.md`) plus an implementation-context specialist.
170
169
  2. Plan: `planner` drafts; `qa-reviewer` challenges assumptions and test shape.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: mike
3
- description: Mike — workflow-first orchestrator with a tiny direct-edit exception.
4
- tools: Workflow, Agent, Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, Skill
3
+ description: Mike — adaptive orchestrator that selects the lightest safe coordination mode.
4
+ tools: Workflow, Agent, Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, Skill, AskUserQuestion
5
5
  skills:
6
6
  - i-have-adhd
7
7
  ---
@@ -12,24 +12,29 @@ Follow `_shared/AGENT_BASELINE.md`. You own the user outcome, integration, and
12
12
  final verification. Direct execution is a narrow exception; workflows are the
13
13
  default for meaningful work.
14
14
 
15
- ## Route, then reassess if scope expands
15
+ ## Orient, clarify, then select the coordination mode
16
16
 
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
- | Bounded workflow | known non-trivial implementation, including a logical bug or any behavioral change | invoke `bizar-implement`; it dispatches at least one editing worker with `isolation: "worktree"`; merge and verify |
21
- | Debug workflow | failing behavior, unclear cause, regression, or interacting state | invoke `bizar-debug`; keep diagnosis and fix evidence separate |
22
- | Research/shaped | external/version-sensitive behavior, architecture/security, broad review, or interacting components | invoke `bizar-research` or the matching `ultracode*` workflow; parallelize independent lanes and serialize dependencies |
23
-
24
- If a request could reasonably require a regression test, touch multiple files,
25
- or needs inspection to discover its scope, it is not tiny: invoke a workflow
26
- before editing. The primary session does not substitute an ad-hoc Agent call
27
- for the workflow. Research current official docs only for external or
20
+ | Single isolated worker | one bounded implementation after scope is clear | dispatch one worktree-isolated Agent with an explicit Bizar model; integrate and verify |
21
+ | Native workflow | repeatable diagnosis, research, review, or an implementation needing visible phase barriers | invoke the matching Bizar workflow with explicit Bizar routing |
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
+ | Parallel agents | two disjoint writable scopes with no cross-talk needed | dispatch concurrently with explicit models and worktree isolation |
24
+
25
+ For every non-tiny request, first make only enough read-only inspection to
26
+ understand the repository boundary and current constraints. Then ask the user
27
+ one concise clarification checkpoint: state the inferred outcome, the material
28
+ choice or risk, and the proposed coordination mode. Wait for the answer before
29
+ writing, dispatching editors, creating branches, or running tests. If the user
30
+ explicitly says to proceed without questions, record that choice and continue.
31
+ After the answer, work autonomously until the requested outcome and verification
32
+ are complete. Research current official docs only for external or
28
33
  version-sensitive claims. Inspect installed skills before hard or specialized
29
34
  work; if stuck with no match, search skills.sh and review the candidate before
30
35
  proposing installation.
31
36
 
32
- Before every Workflow call, read the global Bizar model router and construct a
37
+ Before every Workflow, Agent, or Agent-team call, read the global Bizar model router and construct a
33
38
  small `args.routing` object whose `default`, `medium`, and `high` values are
34
39
  explicit enabled configured model IDs (user picks win; otherwise use enabled
35
40
  tier candidates). Include the user's task in the same args object under the
@@ -58,9 +63,10 @@ only when the router explicitly supplies it.
58
63
 
59
64
  ## Worktree Discipline and integration
60
65
 
61
- Every editing subagent call uses call-level `isolation: "worktree"`. Parallel
62
- writers receive disjoint file ownership and sibling scopes. Read-only research
63
- stays foreground. When a writer finishes, merge its queued branch with
66
+ Every editing subagent call uses call-level `isolation: "worktree"`. Use teams
67
+ only when collaboration changes the result; do not manufacture a team or a
68
+ workflow for a simple isolated task. Parallel writers receive disjoint file
69
+ ownership and sibling scopes. Read-only research stays foreground. When a writer finishes, merge its queued branch with
64
70
  `bizar worktree-merge`; report conflicts instead of guessing. The integration
65
71
  branch runs final tests once after all required results are incorporated.
66
72
  Worktree branches use `wt/<agent_type>-<short-task-id>`.
@@ -43,7 +43,8 @@ Every plan you produce MUST follow this shape. The user (or Mike, or any downstr
43
43
 
44
44
  ## Subagent Model Selection
45
45
 
46
- When you delegate implementation, you recommend the model tier. Read `.claude/model-router.json` to confirm.
46
+ When you delegate implementation, you recommend the model tier. Read the
47
+ operator-global Bizar router (`$BIZAR_HOME/config/claude/model-router.json`) to confirm.
47
48
 
48
49
  | Task shape | Route to | Tier |
49
50
  |---|---|---|
@@ -11,7 +11,7 @@ All hooks read Claude Code JSON from stdin and emit either no decision, addition
11
11
  | `simplify-guard.mjs` | PostToolUse Skill + PreToolUse Bash | require one `/simplify` per commit attempt |
12
12
  | `posttooluse-editwrite.mjs` | PostToolUse writes | local telemetry and test reminder |
13
13
  | `worker-suggest.mjs` | UserPromptSubmit | ranked skill/agent suggestions |
14
- | `workflow-route-guard.mjs` | UserPromptSubmit, PreToolUse, PostToolUse | requires a proven successful native workflow before substantive primary mutation; permits narrowly whitelisted, redirect-free Git inspection |
14
+ | `workflow-route-guard.mjs` | UserPromptSubmit, PreToolUse, PostToolUse | records adaptive Bizar routing state without blocking Mike's selected coordination mode; workflow success clears the pending route record |
15
15
  | `thinking-route.mjs` | UserPromptSubmit | slash and mental-model routing |
16
16
  | `telemetry.mjs` | SessionStart/UserPromptSubmit | local correlation and rejection categories |
17
17
  | `sessionstart-prime.mjs` | SessionStart | bounded project and handoff context |
@@ -44,9 +44,11 @@
44
44
  */
45
45
 
46
46
  import { readFileSync } from 'node:fs';
47
+ import { join } from 'node:path';
47
48
  import { pathToFileURL } from 'node:url';
48
49
 
49
50
  import { loadModelRouter } from '../../../config/agents/model-assignment.mjs';
51
+ import { resolveClaudeConfigDir } from '../../../cli/config-paths.mjs';
50
52
 
51
53
  function deny(reason) {
52
54
  return {
@@ -152,6 +154,17 @@ function readFailoverBlock(toolInput) {
152
154
  };
153
155
  }
154
156
 
157
+ function readConfiguredParentModel(options = {}) {
158
+ if (typeof options.parentModel === 'string') return options.parentModel.trim();
159
+ const settingsPath = options.settingsPath || join(resolveClaudeConfigDir(), 'settings.json');
160
+ try {
161
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
162
+ return typeof settings?.model === 'string' ? settings.model.trim() : '';
163
+ } catch {
164
+ return '';
165
+ }
166
+ }
167
+
155
168
  export async function guardAgentModel(input, options = {}) {
156
169
  if (!input || typeof input !== 'object') return {};
157
170
  if (input.hook_event_name !== 'PreToolUse' || input.tool_name !== 'Agent') return {};
@@ -160,10 +173,6 @@ export async function guardAgentModel(input, options = {}) {
160
173
  const failoverBlock = readFailoverBlock(toolInput);
161
174
  const hasFailoverContract = Boolean(failoverBlock.routingDecisionId) && Boolean(failoverBlock.fallback);
162
175
 
163
- if (!requested || requested === 'inherit') {
164
- return deny('Bizar Agent dispatch blocked: every Agent call requires an explicit enabled model from `bizar models`; session/provider inheritance is prohibited.');
165
- }
166
-
167
176
  let registry;
168
177
  try {
169
178
  registry = options.registry || loadModelRouter(options.routerPath);
@@ -181,6 +190,20 @@ export async function guardAgentModel(input, options = {}) {
181
190
  const allowed = configuredModels(registry);
182
191
  const userPicks = userSelectedModels(registry);
183
192
 
193
+ // The native Agent tool rejects arbitrary gateway IDs in its `model` enum.
194
+ // Inheritance is safe only when the auditable Bizar selection equals the
195
+ // actual global Claude parent model and is a selected, enabled user pick.
196
+ if (!requested || requested === 'inherit') {
197
+ const configured = typeof toolInput.additionalContext?.bizarConfiguredModel === 'string'
198
+ ? toolInput.additionalContext.bizarConfiguredModel.trim()
199
+ : '';
200
+ const parent = readConfiguredParentModel(options);
201
+ if (!configured || configured !== parent || !userPicks.has(configured)) {
202
+ return deny('Bizar Agent dispatch blocked: native Agent inheritance requires the global Claude parent model to exactly match an enabled Bizar user selection. Run `bizar models` and restart Claude Code.');
203
+ }
204
+ return {};
205
+ }
206
+
184
207
  // F-185 contract: when the orchestrator passes both `routingDecisionId`
185
208
  // and `fallback`, validate the fallback against the userSelected pool
186
209
  // but skip the live-discovery re-probe. The fallback's eligibility was
@@ -61,16 +61,12 @@ function readRouterPath() {
61
61
 
62
62
  /**
63
63
  * 10.22.0 / Phase 4: read the operator's `disabledProviders: string[]`
64
- * list with the same dual-path contract as `cli/commands/models.mjs`.
65
- * The Bizar path (`~/.config/bizar/config/claude/model-router.json`) wins
66
- * when both exist (even with explicit `[]`); the legacy
67
- * `~/.claude/model-router.json` mirror is the fallback. Whitespace +
68
- * lowercase normalization happens here. Returns `[]` on any failure —
64
+ * list from the sole global Bizar router. Whitespace + lowercase
65
+ * normalization happens here. Returns `[]` on any failure —
69
66
  * the hook is advisory and must NEVER block session start.
70
67
  */
71
68
  function readDisabledProviders() {
72
69
  const bizarPath = readRouterPath();
73
- const legacyPath = join(resolveClaudeConfigDir(), 'model-router.json');
74
70
  const extract = (parsed) => {
75
71
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
76
72
  if (!Array.isArray(parsed.disabledProviders)) return null;
@@ -85,18 +81,12 @@ function readDisabledProviders() {
85
81
  };
86
82
  if (existsSync(bizarPath)) {
87
83
  const parsed = readJsonIfObject(bizarPath);
88
- // Bizar path exists its `disabledProviders` is authoritative even
89
- // when explicit `[]`. Missing key still falls back to the legacy.
84
+ // An explicit empty global list is a valid operator policy.
90
85
  if (parsed && Array.isArray(parsed.disabledProviders)) {
91
86
  const extracted = extract(parsed);
92
87
  if (extracted !== null) return extracted;
93
88
  }
94
89
  }
95
- const legacy = readJsonIfObject(legacyPath);
96
- if (legacy && Array.isArray(legacy.disabledProviders)) {
97
- const extracted = extract(legacy);
98
- if (extracted !== null) return extracted;
99
- }
100
90
  return [];
101
91
  }
102
92
 
@@ -193,8 +183,7 @@ function syncOnce() {
193
183
  : {};
194
184
  // 10.22.0 / Phase 4: filter the operator's disabled-provider ids out
195
185
  // of the SessionStart re-apply so Claude Code's `/model` picker never
196
- // surfaces e.g. `anthropic/*` after a session restart. Dual-path read
197
- // matches `cli/commands/models.mjs#readDisabledProviders`.
186
+ // surfaces e.g. `anthropic/*` after a session restart.
198
187
  const disabled = readDisabledProviders();
199
188
  const selectedIds = filterDisabled(
200
189
  models.filter((id) => typeof id === 'string' && id.trim()),
@@ -186,13 +186,13 @@ function startupBriefing(cwd, featureBrief, recentCommits, projectLine, progress
186
186
  }
187
187
  }
188
188
  if (progressLast) lines.push(`- Progress: ${progressLast}.`);
189
- lines.push('- You are @mike. Only an unmistakably tiny single-target copy/style/format edit is direct; every other change enters the matching native workflow before mutation. Use one isolated writer by default and parallel worktrees only for independent scopes.');
189
+ lines.push('- You are @mike. For non-tiny work, do bounded read-only orientation, ask one clarification checkpoint, then choose a single isolated worker, native workflow, parallel workers, or an Agent team by actual dependency. Use worktrees for editors and explicit Bizar models for every Agent.');
190
190
  lines.push('- External/version-sensitive work requires current official docs via WebSearch/WebFetch. Use relevant installed skills; apply i-have-adhd to user output. WIP=1.');
191
191
  lines.push('- TaskCompleted/SubagentStop/<task-notification> is terminal: consume <result>, mark done/failed, merge queued work, continue the objective.');
192
192
  // Default-first-stop hint when nothing is active yet.
193
193
  if (featureBrief && featureBrief.active.length === 0) {
194
194
  lines.push(
195
- '- First move: read PROGRESS.md and feature_list.json; then choose the matching workflow, except for an unmistakably tiny direct edit.',
195
+ '- First move: read PROGRESS.md and feature_list.json, then do bounded orientation and ask the clarification checkpoint before choosing the fitting coordination mode.',
196
196
  );
197
197
  }
198
198
  return lines.join('\n');
@@ -203,14 +203,14 @@ function clearBriefing(cwd, recentCommits, progressLast) {
203
203
  if (progressLast) lines.push(`- Progress: ${progressLast}.`);
204
204
  if (recentCommits.length > 0) lines.push(`- Last commit: ${recentCommits[0]}.`);
205
205
  lines.push('- Context preserved in same repo / cwd — only the model turn was reset.');
206
- lines.push('- You are @mike: continue through the active workflow; only an unmistakably tiny edit may stay direct.');
206
+ lines.push('- You are @mike: continue the active coordination plan; adapt it when new evidence changes the fit.');
207
207
  lines.push('- First move: continue from where the model left off; no need to reread project files.');
208
208
  return lines.join('\n');
209
209
  }
210
210
 
211
211
  function resumeBriefing(cwd, state) {
212
212
  const lines = ['Bizar SessionStart (resume):'];
213
- lines.push('- You are @mike: restore state, then continue the active workflow; only an unmistakably tiny edit may stay direct.');
213
+ lines.push('- You are @mike: restore state, then continue the active coordination plan and adapt it if the evidence changed.');
214
214
  if (state) {
215
215
  if (state.activeFeature) lines.push(`- Last active feature: ${state.activeFeature}.`);
216
216
  if (state.reason) lines.push(`- Last session ended with: ${state.reason}.`);
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * Runs on every user prompt. Only unmistakably tiny, single-scope edits take
8
8
  * a cheap fast path. Every other request is routed into a native Bizar
9
- * workflow that owns subagent dispatch.
9
+ * adaptive coordination mode selected by Mike after bounded orientation and a
10
+ * clarification checkpoint.
10
11
  *
11
12
  * Uses import.meta.url + dynamic import() to resolve the sibling CLI module so
12
13
  * the hook works regardless of install path (fixes ERR_MODULE_NOT_FOUND after
@@ -56,10 +57,10 @@ const FAST_ROUTE_POLICY = [
56
57
  ].join('\n');
57
58
 
58
59
  const ROUTE_POLICY = [
59
- 'Workflow-required Bizar routing policy:',
60
- '- If this is the primary session, you ARE @mike. Do not implement this request directly in the primary session. Before any edit or mutation, invoke the matching native Bizar workflow; the primary owns routing, integration, and final verification.',
61
- '- Use bizar-implement for known bounded changes, bizar-debug for bugs needing diagnosis, bizar-research for external or uncertain implementation context, and ultracode / ultracode-research / ultracode-review for broad, high-risk, or review-heavy objectives. Use only phases that reduce a concrete risk.',
62
- '- The workflow must dispatch at least one implementation subagent with an explicit configured model and call-level `isolation: "worktree"`. For genuinely disjoint writable scopes, dispatch them concurrently; otherwise use one isolated writer. Never create duplicate workers merely to satisfy fan-out.',
60
+ 'Adaptive Bizar routing policy:',
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
+ '- 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 explicit enabled configured Bizar model; use no provider alias, inheritance, or default. Every editing worker uses call-level `isolation: "worktree"`. For genuinely disjoint writable scopes, dispatch concurrently; otherwise use one owner.',
63
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.',
64
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.',
65
66
  '- If you are already running as a Bizar custom agent, follow your assigned role and do not recursively dispatch yourself.',
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /** Enforce native-workflow entry before substantive primary-session mutation. */
2
+ /** Preserve a mutation barrier while Mike selects an adaptive coordination mode. */
3
3
  'use strict';
4
4
 
5
5
  import {
@@ -13,6 +13,9 @@ import { parseGitCommands } from './git-command-parser.mjs';
13
13
  const READ_ONLY_GIT_SUBCOMMANDS = new Set([
14
14
  'diff', 'log', 'ls-files', 'rev-parse', 'show', 'status',
15
15
  ]);
16
+ const READ_ONLY_BRANCH_OPTIONS = new Set([
17
+ '--all', '--list', '--remotes', '--show-current', '--verbose', '-a', '-r', '-v', '-vv',
18
+ ]);
16
19
  const WORKFLOW_SUCCESS = new Set(['completed', 'dry', 'ready-for-integration', 'succeeded', 'success']);
17
20
  const WORKFLOW_FAILURE = new Set(['blocked', 'budget-exhausted', 'cancelled', 'canceled', 'error', 'failed', 'failure']);
18
21
 
@@ -22,10 +25,17 @@ export function isReadOnlyShellInspection(command) {
22
25
 
23
26
  return source.split(/\s*&&\s*/).every((segment) => {
24
27
  const trimmed = segment.trim();
25
- if (/^echo(?:\s|$)/.test(trimmed)) return true;
28
+ // These commands can inspect a newly opened project but cannot mutate it.
29
+ // Permit them before workflow launch so the primary can choose the right
30
+ // native workflow instead of deadlocking on its standard orientation step.
31
+ if (/^(?:echo|ls)(?:\s|$)/.test(trimmed) || trimmed === 'pwd') return true;
26
32
  if (!/^git(?:\s|$)/.test(trimmed) || /(?:^|\s)--(?:output|ext-diff)(?:=|\s|$)/.test(trimmed)) return false;
27
33
  const parsed = parseGitCommands(trimmed);
28
- return parsed.length === 1 && READ_ONLY_GIT_SUBCOMMANDS.has(parsed[0].subcommand);
34
+ if (parsed.length !== 1) return false;
35
+ const [git] = parsed;
36
+ if (READ_ONLY_GIT_SUBCOMMANDS.has(git.subcommand)) return true;
37
+ return git.subcommand === 'branch'
38
+ && git.args.every((argument) => READ_ONLY_BRANCH_OPTIONS.has(argument));
29
39
  });
30
40
  }
31
41
 
@@ -77,17 +87,10 @@ process.stdin.on('end', () => {
77
87
  return;
78
88
  }
79
89
 
80
- const readOnlyBash = toolName === 'Bash' && isReadOnlyShellInspection(input.tool_input?.command);
81
- if (!readOnlyBash && /^(?:Edit|Write|MultiEdit|Bash|Agent)$/.test(toolName)) {
82
- process.stdout.write(`${JSON.stringify({
83
- hookSpecificOutput: {
84
- hookEventName: 'PreToolUse',
85
- permissionDecision: 'deny',
86
- permissionDecisionReason: `Bizar workflow routing guard: ${toolName} is unavailable in the primary session until the required native Workflow runs successfully. Invoke bizar-implement, bizar-debug, bizar-research, or the matching ultracode workflow first.`,
87
- },
88
- })}\n`);
89
- return;
90
- }
90
+ // Coordination is an orchestrator decision, not a hard-coded tool gate.
91
+ // Instructions require orientation + clarification before mutation; Agent
92
+ // calls are independently protected by agent-model-guard.mjs. Keeping this
93
+ // hook advisory avoids trapping valid single-worker and Agent-team plans.
91
94
  } catch (error) {
92
95
  process.stderr.write(`[bizar.workflow-route] ${error?.message || String(error)}\n`);
93
96
  }
@@ -3,7 +3,10 @@ export const meta = {
3
3
  description: 'Implement one bounded change in one worktree, or run explicitly supplied disjoint lanes concurrently',
4
4
  whenToUse: 'Use when implementation scope is understood and external research or root-cause discovery is unnecessary.',
5
5
  phases: [
6
- { title: 'Implement', detail: 'Run one isolated writer, or explicit disjoint writers concurrently' },
6
+ { title: 'Scope', detail: 'Independently confirm the local implementation boundary and risks' },
7
+ { title: 'Plan', detail: 'Turn the bounded objective into owned edit lanes and checks' },
8
+ { title: 'Implement', detail: 'Run isolated writers for the approved lanes' },
9
+ { title: 'Review', detail: 'Independently verify each implementation result before integration' },
7
10
  ],
8
11
  }
9
12
 
@@ -52,9 +55,20 @@ const TOPIC = typeof args === 'string'
52
55
  : JSON.stringify(args || {})
53
56
  const SCOPE = (args && Array.isArray(args.scope)) ? args.scope : []
54
57
  const suppliedLanes = (args && Array.isArray(args.lanes)) ? args.lanes : []
58
+ const fallbackLanes = [{ name: 'bounded-change', scope: SCOPE, task: TOPIC }]
59
+
60
+ phase('Scope')
61
+ const scopeEvidence = await parallel([
62
+ () => dispatchAgent(agent, 'scope-researcher', `Confirm the smallest repository-local implementation boundary for "${TOPIC}". Identify existing code, tests, configuration, and reusable utilities. Do not edit.`, { role: 'research-analyst', risk: 'medium', capabilities: ['structured-output', 'reasoning'], label: 'scope-repository', phase: 'Scope' }),
63
+ () => dispatchAgent(agent, 'scope-critic', `Independently challenge the assumed scope for "${TOPIC}". Identify hidden integration points, ownership conflicts, and the minimum regression evidence needed. Do not edit.`, { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: 'scope-risk', phase: 'Scope' }),
64
+ ])
65
+ const scopeSummary = scopeEvidence.map((entry) => JSON.stringify(entry ?? '')).join('\n').slice(0, 4000)
66
+
67
+ phase('Plan')
68
+ const plan = await dispatchAgent(agent, 'bounded-planner', `Produce a minimal reversible plan for "${TOPIC}". Return disjoint edit lanes with a single owner for shared files, plus the smallest proving tests. Do not edit.\n${barrierRef({ phase: 'Scope', label: 'scope-evidence', summary: scopeSummary, payload: scopeEvidence }).promptBlock}`, { role: 'architect', risk: 'medium', capabilities: ['structured-output', 'reasoning', 'architecture'], label: 'plan', phase: 'Plan' })
55
69
  const lanes = (suppliedLanes.length > 0
56
70
  ? suppliedLanes
57
- : [{ name: 'bounded-change', scope: SCOPE, task: TOPIC }]
71
+ : (Array.isArray(plan?.lanes) && plan.lanes.length > 0 ? plan.lanes : fallbackLanes)
58
72
  ).slice(0, 6)
59
73
 
60
74
  phase('Implement')
@@ -70,14 +84,23 @@ const implementations = lanes.length === 1
70
84
  : await parallel(lanes.map((lane, index) => () => runLane(lane, index)))
71
85
  const completed = implementations.filter(Boolean)
72
86
 
73
- if (completed.length === 0) {
74
- return { status: 'blocked', reason: 'No implementation lane completed successfully.', lanes }
75
- }
87
+ if (completed.length === 0) return { status: 'blocked', reason: 'No implementation lane completed successfully.', scopeEvidence, plan, lanes }
88
+
89
+ phase('Review')
90
+ const reviews = await parallel(completed.map((implementation, index) => () => dispatchAgent(
91
+ agent,
92
+ `implementation-reviewer-${index + 1}`,
93
+ `Review implementation lane "${lanes[index]?.name || index + 1}" for "${TOPIC}". Verify scope, correctness, regression evidence, and integration assumptions. Report only actionable defects and required checks; do not edit.\n${barrierRef({ phase: 'Implement', label: `implement:${index + 1}:${lanes[index]?.name || 'bounded-change'}`, summary: typeof implementation === 'string' ? implementation.slice(0, 400) : 'implementation artifact', payload: implementation }).promptBlock}`,
94
+ { role: 'adversarial', risk: 'high', capabilities: ['structured-output', 'reasoning'], label: `review:${index + 1}:${lanes[index]?.name || 'bounded-change'}`, phase: 'Review' },
95
+ )))
76
96
 
77
97
  return {
78
98
  status: 'ready-for-integration',
79
99
  topic: TOPIC,
100
+ scopeEvidence,
101
+ plan,
80
102
  lanes,
81
103
  implementations: completed,
104
+ reviews: reviews.filter(Boolean),
82
105
  next: 'Merge queued worktrees and run integration verification in the primary session.',
83
106
  }
@@ -793,15 +793,17 @@ export function classifyDispatchOutcome(result, error, startMs) {
793
793
  }
794
794
 
795
795
  /**
796
- * Build the augmented agent-call payload. Adds `model`,
797
- * `routingDecisionId`, `tier`, and `selectorReason` so the Agent
798
- * tool can route correctly and the audit trail can prove which
799
- * selector step fired.
796
+ * Build the augmented agent-call payload. The native Agent model field cannot
797
+ * carry arbitrary gateway IDs, so the selected Bizar ID is audit context and
798
+ * the guard permits inheritance only from an identical configured parent.
800
799
  */
801
800
  export function augmentPayload(opts, decision, agentName) {
802
801
  return {
803
802
  ...opts,
804
- model: decision.modelId ?? undefined,
803
+ additionalContext: {
804
+ ...(opts.additionalContext && typeof opts.additionalContext === 'object' ? opts.additionalContext : {}),
805
+ bizarConfiguredModel: decision.modelId ?? null,
806
+ },
805
807
  routingDecisionId: decision.routingDecisionId,
806
808
  tier: decision.tier,
807
809
  selectorReason: decision.reason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.23.8",
3
+ "version": "10.23.10",
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": {
@@ -25,7 +25,6 @@
25
25
  "config/claude/hooks/*.mjs",
26
26
  "config/claude/hooks/*.sh",
27
27
  "config/claude/hooks/README.md",
28
- "config/claude/model-router.json",
29
28
  "config/claude/settings.json",
30
29
  "scripts/git-hooks/commit-msg",
31
30
  "scripts/git-hooks/pre-commit",
@@ -35,11 +34,11 @@
35
34
  ],
36
35
  "scripts": {
37
36
  "typecheck": "tsc --noEmit",
38
- "build:sdk": "node scripts/build-sdk.mjs",
37
+ "build:sdk": "node scripts/with-sdk-dist-lock.mjs node scripts/build-sdk.mjs",
39
38
  "test:sdk": "vitest run --root packages/sdk",
40
39
  "test:sdk:watch": "vitest --root packages/sdk",
41
40
  "test:node": "node scripts/run-node-tests.mjs",
42
- "test": "npm run build:sdk && npm run test:sdk && npm run test:node",
41
+ "test": "node scripts/with-sdk-dist-lock.mjs node scripts/run-test-with-sdk-build.mjs",
43
42
  "build": "npm run build:sdk",
44
43
  "prepack": "npm run build",
45
44
  "prepublishOnly": "npm run build",
@@ -385,7 +385,7 @@ const bizarAuditTool = defineTool("bizar_audit", "Wrapper around `bizar audit --
385
385
  }
386
386
  }, { readOnlyHint: true });
387
387
  // `bizar model list --json` — gateway model inventory.
388
- const bizarModelListTool = defineTool("bizar_model_list", "Wrapper around `bizar models --json`. Returns ONLY the models the user has explicitly enabled via `bizar models` (the `userSelected` block of `config/claude/model-router.json`). Live discovery is filtered out by default to avoid surfacing dozens of unrelated models. Empty list = orchestrator inherits the active session model.", {}, async () => {
388
+ const bizarModelListTool = defineTool("bizar_model_list", "Wrapper around `bizar models --json`. Returns ONLY the models the user has explicitly enabled via `bizar models` in the global Bizar router (`$BIZAR_HOME/config/claude/model-router.json#userSelected`). Live discovery is filtered out by default to avoid surfacing dozens of unrelated models. An empty list means no agent dispatch is permitted until the operator configures models.", {}, async () => {
389
389
  try {
390
390
  // Prefer the new `bizar models` surface; fall back to the legacy
391
391
  // `bizar model list` alias for older installs. The CLI is responsible
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export declare const SDK_VERSION: "10.23.8";
4
+ export declare const SDK_VERSION: "10.23.9";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export const SDK_VERSION = "10.23.8";
4
+ export const SDK_VERSION = "10.23.9";
5
5
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-sdk",
3
- "version": "10.23.8",
3
+ "version": "10.23.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,131 +0,0 @@
1
- {
2
- "$schema": "https://bizar.dev/schema/model-router.v3.json",
3
- "version": "13.0.0",
4
- "endpoint": null,
5
- "disabledProviders": ["anthropic"],
6
- "comment": "Mike selects the cheapest sufficient enabled configured model per dispatch. Explicit user-selected models take precedence; configured dynamic tiers are the fallback pool. The orchestrator always supplies an Agent model and fails closed when no enabled configured candidate exists, so Claude Code cannot select an unconfigured provider default. The gateway endpoint is intentionally null in the shipped config — operators MUST configure it via the `ANTHROPIC_BASE_URL` or `BIZAR_MODEL_ROUTER_URL` environment variable. Bizar is provider-agnostic and ships no default provider. `disabledProviders` (top-level, optional) is a case-insensitive-at-write, case-sensitive-at-filter list of provider prefixes whose model ids are stripped from every read site (CLI sync, picker, settings.json sync, SessionStart hook, Agent model guard). Defaults to `[]`; ships with `[\"anthropic\"]` so a fresh install never dispatches `anthropic/*` ids even if a gateway reports them.",
7
- "userSelected": {
8
- "models": [],
9
- "lastUpdated": null,
10
- "source": "default-empty",
11
- "tierHints": {}
12
- },
13
- "gateway": {
14
- "required": false,
15
- "endpoint": null,
16
- "availabilityProbe": "/models",
17
- "discoveryTimeoutMs": 3000,
18
- "unavailableBehavior": "configured-tier-fallback",
19
- "documentation": "https://code.claude.com/docs/en/llm-gateway"
20
- },
21
- "tiers": {
22
- "premium": {
23
- "models": [
24
- "qct/qwen3.8-max-preview",
25
- "codex/gpt-5.6-sol",
26
- "codex/gpt-5.6-luna"
27
- ],
28
- "purpose": "Architecture, high-risk planning, adversarial verification, and last-resort debugging where mistakes are expensive.",
29
- "effort": "high"
30
- },
31
- "high": {
32
- "models": [
33
- "codex/gpt-5.6-sol",
34
- "qct/qwen3.8-max-preview"
35
- ],
36
- "purpose": "Complex implementation, security review, and cross-cutting reasoning.",
37
- "effort": "high"
38
- },
39
- "mid-design": {
40
- "models": [
41
- "codex/gpt-5.6-luna",
42
- "minimax/MiniMax-M3"
43
- ],
44
- "purpose": "UI/UX design and product-quality implementation where visual judgment matters.",
45
- "effort": "medium"
46
- },
47
- "default": {
48
- "models": [
49
- "minimax/MiniMax-M3",
50
- "minimax/MiniMax-M2.7"
51
- ],
52
- "purpose": "Broad research, git operations, and ordinary engineering work.",
53
- "effort": "medium"
54
- },
55
- "mid": {
56
- "models": [
57
- "minimax/MiniMax-M2.7",
58
- "minimax/MiniMax-M2.7-highspeed",
59
- "qct/deepseek-v4-pro",
60
- "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free"
61
- ],
62
- "purpose": "Bounded technical analysis, code search, and moderate implementation.",
63
- "effort": "medium"
64
- },
65
- "budget": {
66
- "models": [
67
- "glm/glm-5.3-flash",
68
- "minimax/MiniMax-M2.7-highspeed"
69
- ],
70
- "purpose": "Routine deterministic edits, targeted clarification, and scripted verification.",
71
- "effort": "low"
72
- }
73
- },
74
- "roleDefaults": {
75
- "mike": "premium",
76
- "paul": "premium",
77
- "carl": "premium",
78
- "karen": "high",
79
- "linda": "high",
80
- "ria": "mid-design",
81
- "greg": "default",
82
- "steve": "default",
83
- "oscar": "mid",
84
- "todd": "mid",
85
- "susan": "mid",
86
- "pam": "budget",
87
- "brenda": "budget",
88
- "janet": "budget",
89
- "kevin": "budget",
90
- "brad": "mid-design"
91
- },
92
- "complexityRules": [
93
- {
94
- "when": "irreversible, security-sensitive, architectural, or previously failed twice",
95
- "tier": "premium"
96
- },
97
- {
98
- "when": "cross-cutting implementation or adversarial review",
99
- "tier": "high"
100
- },
101
- {
102
- "when": "UI or product design judgment dominates",
103
- "tier": "mid-design"
104
- },
105
- {
106
- "when": "bounded implementation or codebase research",
107
- "tier": "mid"
108
- },
109
- {
110
- "when": "deterministic, mechanical, or clarification-only",
111
- "tier": "budget"
112
- }
113
- ],
114
- "policies": {
115
- "mainOrchestrator": "mike",
116
- "selectionOwner": "orchestrator",
117
- "dispatchModelOverride": "required-configured-candidate",
118
- "unknownAgent": "configured-tier-fallback",
119
- "discoveryFailure": "configured-tier-fallback",
120
- "unavailableModel": "configured-tier-fallback",
121
- "silentProviderSubstitution": false,
122
- "retryModelAliases": false,
123
- "snapshotDecisions": true,
124
- "maxDispatchModelAttempts": 1
125
- },
126
- "costControls": {
127
- "maxConcurrentAgents": 10,
128
- "maxPremiumAgentsPerWorkflow": 3,
129
- "preferInheritanceWhenDiscoveryUnavailable": false
130
- }
131
- }