bosun 0.34.5 → 0.34.7

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.
Files changed (46) hide show
  1. package/agent-hook-bridge.mjs +6 -6
  2. package/agent-pool.mjs +75 -5
  3. package/bosun.schema.json +31 -0
  4. package/cli.mjs +18 -3
  5. package/codex-config.mjs +256 -8
  6. package/config.mjs +54 -8
  7. package/copilot-shell.mjs +75 -5
  8. package/desktop/main.mjs +3 -0
  9. package/git-commit-helpers.mjs +31 -3
  10. package/library-manager.mjs +6 -4
  11. package/monitor.mjs +126 -11
  12. package/package.json +2 -1
  13. package/postinstall.mjs +8 -5
  14. package/primary-agent.mjs +128 -19
  15. package/repo-config.mjs +110 -7
  16. package/setup-web-server.mjs +4 -3
  17. package/setup.mjs +49 -57
  18. package/task-context.mjs +200 -0
  19. package/task-executor.mjs +142 -34
  20. package/telegram-bot.mjs +13 -4
  21. package/ui/app.js +1 -1
  22. package/ui/components/agent-selector.js +17 -7
  23. package/ui/components/chat-view.js +109 -79
  24. package/ui/components/diff-viewer.js +1 -1
  25. package/ui/components/workspace-switcher.js +4 -4
  26. package/ui/modules/agent-display.js +5 -5
  27. package/ui/setup.html +104 -22
  28. package/ui/styles/base.css +1 -1
  29. package/ui/styles/sessions.css +113 -0
  30. package/ui/styles/workspace-switcher.css +10 -0
  31. package/ui/tabs/agents.js +6 -6
  32. package/ui/tabs/chat.js +33 -33
  33. package/ui/tabs/dashboard.js +10 -10
  34. package/ui/tabs/library.js +18 -16
  35. package/ui/tabs/settings.js +161 -15
  36. package/ui/tabs/tasks.js +8 -8
  37. package/ui/tabs/workflows.js +3 -2
  38. package/ui-server.mjs +152 -21
  39. package/workflow-engine.mjs +14 -0
  40. package/workflow-nodes.mjs +537 -43
  41. package/workflow-templates/agents.mjs +70 -4
  42. package/workflow-templates/ci-cd.mjs +14 -7
  43. package/workflow-templates/github.mjs +21 -9
  44. package/workflow-templates/reliability.mjs +283 -0
  45. package/workflow-templates/security.mjs +5 -5
  46. package/workflow-templates.mjs +7 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
4
  import { executeBlockingHooks, executeHooks, loadHooks, registerBuiltinHooks } from "./agent-hooks.mjs";
5
+ import { shouldRunAgentHookBridge } from "./task-context.mjs";
5
6
 
6
7
  const TAG = "[agent-hook-bridge]";
7
8
 
@@ -161,12 +162,11 @@ function mapEvents(sourceEvent, payload) {
161
162
  }
162
163
 
163
164
  async function run() {
164
- // ── VE_MANAGED guard ──────────────────────────────────────────────────────
165
- // Only execute hooks for sessions managed by bosun.
166
- // bosun sets VE_MANAGED=1 in all spawned agent environments.
167
- // If this env var is missing, we're running inside a standalone agent session
168
- // that just happens to have the hook files in its config — exit silently.
169
- if (!process.env.VE_MANAGED && !process.env.BOSUN_HOOKS_FORCE) {
165
+ // ── task-context guard ────────────────────────────────────────────────────
166
+ // Execute hooks only for active Bosun task sessions.
167
+ // This prevents globally scaffolded hook configs from affecting standalone
168
+ // sessions in user repos. BOSUN_HOOKS_FORCE remains an explicit override.
169
+ if (!shouldRunAgentHookBridge(process.env)) {
170
170
  process.exit(0);
171
171
  }
172
172
 
package/agent-pool.mjs CHANGED
@@ -45,6 +45,16 @@ import { loadConfig } from "./config.mjs";
45
45
  import { resolveRepoRoot, resolveAgentRepoRoot } from "./repo-root.mjs";
46
46
  import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
47
47
 
48
+ // Lazy-load MCP registry to avoid circular dependencies.
49
+ // Cached at module scope per AGENTS.md hard rules.
50
+ let _mcpRegistry = null;
51
+ async function getMcpRegistry() {
52
+ if (!_mcpRegistry) {
53
+ _mcpRegistry = await import("./mcp-registry.mjs");
54
+ }
55
+ return _mcpRegistry;
56
+ }
57
+
48
58
  const __filename = fileURLToPath(import.meta.url);
49
59
  const __dirname = dirname(__filename);
50
60
 
@@ -897,7 +907,7 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
897
907
  * Build CLI arguments for ephemeral Copilot agent-pool sessions.
898
908
  * Mirrors copilot-shell.mjs buildCliArgs() for feature parity.
899
909
  */
900
- function buildPoolCopilotCliArgs() {
910
+ function buildPoolCopilotCliArgs(mcpConfigPath) {
901
911
  const args = [];
902
912
  if (!envFlagEnabled(process.env.COPILOT_NO_EXPERIMENTAL)) {
903
913
  args.push("--experimental");
@@ -915,9 +925,10 @@ function buildPoolCopilotCliArgs() {
915
925
  if (envFlagEnabled(process.env.COPILOT_DISABLE_BUILTIN_MCPS)) {
916
926
  args.push("--disable-builtin-mcps");
917
927
  }
918
- const mcpConfigPath = process.env.COPILOT_ADDITIONAL_MCP_CONFIG;
919
- if (mcpConfigPath) {
920
- args.push("--additional-mcp-config", mcpConfigPath);
928
+ // MCP config: prefer per-launch resolved config, fall back to env var
929
+ const effectiveMcpConfig = mcpConfigPath || process.env.COPILOT_ADDITIONAL_MCP_CONFIG;
930
+ if (effectiveMcpConfig) {
931
+ args.push("--additional-mcp-config", effectiveMcpConfig);
921
932
  }
922
933
  return args;
923
934
  }
@@ -1024,7 +1035,17 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
1024
1035
  clientOpts = { cliUrl, env: clientEnv };
1025
1036
  } else {
1026
1037
  // Local mode (default): stdio for full capability
1027
- const cliArgs = buildPoolCopilotCliArgs();
1038
+ // Write temp MCP config if resolved MCP servers are available
1039
+ let mcpConfigPath = null;
1040
+ if (extra._resolvedMcpServers?.length) {
1041
+ try {
1042
+ const registry = await getMcpRegistry();
1043
+ mcpConfigPath = registry.writeTempCopilotMcpConfig(cwd, extra._resolvedMcpServers);
1044
+ } catch (mcpErr) {
1045
+ console.warn(`${TAG} copilot MCP config write failed (non-fatal): ${mcpErr.message}`);
1046
+ }
1047
+ }
1048
+ const cliArgs = buildPoolCopilotCliArgs(mcpConfigPath);
1028
1049
  clientOpts = {
1029
1050
  cwd,
1030
1051
  env: clientEnv,
@@ -1465,6 +1486,27 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
1465
1486
  }
1466
1487
 
1467
1488
  // ── 4. Execute query ─────────────────────────────────────────────────────
1489
+ // Inject MCP server config for Claude via environment variable
1490
+ let _claudeMcpEnvCleanup = null;
1491
+ if (extra._resolvedMcpServers?.length) {
1492
+ try {
1493
+ const registry = await getMcpRegistry();
1494
+ const { envVar } = registry.buildClaudeMcpEnv(extra._resolvedMcpServers);
1495
+ if (envVar) {
1496
+ const prev = process.env.CLAUDE_MCP_SERVERS;
1497
+ process.env.CLAUDE_MCP_SERVERS = envVar;
1498
+ _claudeMcpEnvCleanup = () => {
1499
+ if (prev === undefined) {
1500
+ delete process.env.CLAUDE_MCP_SERVERS;
1501
+ } else {
1502
+ process.env.CLAUDE_MCP_SERVERS = prev;
1503
+ }
1504
+ };
1505
+ }
1506
+ } catch (mcpErr) {
1507
+ console.warn(`${TAG} claude MCP env setup failed (non-fatal): ${mcpErr.message}`);
1508
+ }
1509
+ }
1468
1510
  try {
1469
1511
  const msgQueue = createMessageQueue();
1470
1512
 
@@ -1596,6 +1638,7 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
1596
1638
 
1597
1639
  const output =
1598
1640
  finalResponse.trim() || "(Agent completed with no text output)";
1641
+ if (typeof _claudeMcpEnvCleanup === "function") _claudeMcpEnvCleanup();
1599
1642
  return {
1600
1643
  success: true,
1601
1644
  output,
@@ -1605,6 +1648,7 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
1605
1648
  threadId: activeClaudeSessionId,
1606
1649
  };
1607
1650
  } catch (err) {
1651
+ if (typeof _claudeMcpEnvCleanup === "function") _claudeMcpEnvCleanup();
1608
1652
  clearAbortScope();
1609
1653
  if (hardTimer) clearTimeout(hardTimer);
1610
1654
  if (steerKey) unregisterActiveSession(steerKey);
@@ -1714,6 +1758,32 @@ export async function launchEphemeralThread(
1714
1758
  timeoutMs = DEFAULT_TIMEOUT_MS,
1715
1759
  extra = {},
1716
1760
  ) {
1761
+ // ── Resolve MCP servers for this launch ──────────────────────────────────
1762
+ try {
1763
+ if (!extra._mcpResolved) {
1764
+ const cfg = loadConfig();
1765
+ const mcpCfg = cfg.mcpServers || {};
1766
+ if (mcpCfg.enabled !== false) {
1767
+ const requestedIds = extra.mcpServers || [];
1768
+ const defaultIds = mcpCfg.defaultServers || [];
1769
+ if (requestedIds.length || defaultIds.length) {
1770
+ const registry = await getMcpRegistry();
1771
+ const resolved = await registry.resolveMcpServersForAgent(
1772
+ cwd,
1773
+ requestedIds,
1774
+ { defaultServers: defaultIds, catalogOverrides: mcpCfg.catalogOverrides || {} },
1775
+ );
1776
+ if (resolved.length) {
1777
+ extra._resolvedMcpServers = resolved;
1778
+ }
1779
+ }
1780
+ }
1781
+ extra._mcpResolved = true;
1782
+ }
1783
+ } catch (mcpErr) {
1784
+ console.warn(`${TAG} MCP server resolution failed (non-fatal): ${mcpErr.message}`);
1785
+ }
1786
+
1717
1787
  // Determine the primary SDK to try
1718
1788
  const requestedSdk = extra.sdk
1719
1789
  ? String(extra.sdk).trim().toLowerCase()
package/bosun.schema.json CHANGED
@@ -606,6 +606,37 @@
606
606
  "type": "object",
607
607
  "description": "Hook definitions loaded by agent-hooks.mjs (event -> hook list).",
608
608
  "additionalProperties": true
609
+ },
610
+ "mcpServers": {
611
+ "type": "object",
612
+ "description": "MCP (Model Context Protocol) server management. Controls which MCP servers are available to agents.",
613
+ "additionalProperties": false,
614
+ "properties": {
615
+ "enabled": {
616
+ "type": "boolean",
617
+ "default": true,
618
+ "description": "Enable MCP server integration for agent launches"
619
+ },
620
+ "defaultServers": {
621
+ "type": "array",
622
+ "items": { "type": "string" },
623
+ "description": "MCP server IDs attached to every agent launch by default (e.g. [\"context7\", \"microsoft-docs\"])"
624
+ },
625
+ "catalogOverrides": {
626
+ "type": "object",
627
+ "additionalProperties": {
628
+ "type": "object",
629
+ "description": "Per-server environment variable overrides",
630
+ "additionalProperties": { "type": "string" }
631
+ },
632
+ "description": "Environment variable overrides keyed by MCP server ID"
633
+ },
634
+ "autoInstallDefaults": {
635
+ "type": "boolean",
636
+ "default": true,
637
+ "description": "Automatically install defaultServers from catalog if not already installed"
638
+ }
639
+ }
609
640
  }
610
641
  }
611
642
  }
package/cli.mjs CHANGED
@@ -1484,14 +1484,21 @@ function detectExistingMonitorLockOwner(excludePid = null) {
1484
1484
  return null;
1485
1485
  }
1486
1486
 
1487
- function runMonitor() {
1487
+ function runMonitor({ restartReason = "" } = {}) {
1488
1488
  return new Promise((resolve, reject) => {
1489
1489
  const monitorPath = fileURLToPath(
1490
1490
  new URL("./monitor.mjs", import.meta.url),
1491
1491
  );
1492
+ const childEnv = { ...process.env };
1493
+ if (restartReason) {
1494
+ childEnv.BOSUN_MONITOR_RESTART_REASON = restartReason;
1495
+ } else {
1496
+ delete childEnv.BOSUN_MONITOR_RESTART_REASON;
1497
+ }
1492
1498
  monitorChild = fork(monitorPath, process.argv.slice(2), {
1493
1499
  stdio: "inherit",
1494
1500
  execArgv: ["--max-old-space-size=4096"],
1501
+ env: childEnv,
1495
1502
  windowsHide: IS_DAEMON_CHILD && process.platform === "win32",
1496
1503
  });
1497
1504
  daemonCrashTracker.markStart();
@@ -1504,7 +1511,7 @@ function runMonitor() {
1504
1511
  "\n \u21BB Monitor restarting with fresh modules...\n",
1505
1512
  );
1506
1513
  // Small delay to let file writes / port releases settle
1507
- setTimeout(() => resolve(runMonitor()), 2000);
1514
+ setTimeout(() => resolve(runMonitor({ restartReason: "self-restart" })), 2000);
1508
1515
  } else {
1509
1516
  const exitCode = code ?? (signal ? 1 : 0);
1510
1517
  const existingOwner =
@@ -1572,7 +1579,15 @@ function runMonitor() {
1572
1579
  restartAttempt: daemonRestartCount,
1573
1580
  maxRestarts: IS_DAEMON_CHILD ? DAEMON_MAX_RESTARTS : 0,
1574
1581
  }).catch(() => {});
1575
- setTimeout(() => resolve(runMonitor()), delayMs);
1582
+ setTimeout(
1583
+ () =>
1584
+ resolve(
1585
+ runMonitor({
1586
+ restartReason: isOSKill ? "os-kill" : "crash",
1587
+ }),
1588
+ ),
1589
+ delayMs,
1590
+ );
1576
1591
  return;
1577
1592
  }
1578
1593
 
package/codex-config.mjs CHANGED
@@ -2,12 +2,15 @@
2
2
  * codex-config.mjs — Manages the Codex CLI config (~/.codex/config.toml)
3
3
  *
4
4
  * Ensures the user's Codex CLI configuration has:
5
- * 1. A vibe_kanban MCP server section with the correct env vars
6
- * 2. Sufficient stream_idle_timeout_ms on all model providers
7
- * 3. Recommended defaults for long-running agentic workloads
8
- * 4. Feature flags for sub-agents, memory, undo, collaboration
9
- * 5. Sandbox permissions and shell environment policy
10
- * 6. Common MCP servers (context7, microsoft-docs)
5
+ * 1. Sufficient stream_idle_timeout_ms on all model providers
6
+ * 2. Recommended defaults for long-running agentic workloads
7
+ * 3. Feature flags for sub-agents, memory, undo, collaboration
8
+ * 4. Sandbox permissions and shell environment policy
9
+ * 5. Common MCP servers (context7, microsoft-docs)
10
+ *
11
+ * NOTE: Vibe-Kanban MCP is workspace-scoped and managed by repo-config.mjs
12
+ * inside each repo's `.codex/config.toml`. Global config no longer auto-adds
13
+ * `[mcp_servers.vibe_kanban]`.
11
14
  *
12
15
  * SCOPE: This manages the GLOBAL ~/.codex/config.toml which contains:
13
16
  * - Model provider configs (API keys, base URLs) — MUST be global
@@ -887,14 +890,17 @@ export function buildCommonMcpBlocks() {
887
890
  "",
888
891
  "# ── Common MCP servers (added by bosun) ──",
889
892
  "[mcp_servers.context7]",
893
+ "startup_timeout_sec = 120",
890
894
  'command = "npx"',
891
895
  'args = ["-y", "@upstash/context7-mcp"]',
892
896
  "",
893
897
  "[mcp_servers.sequential-thinking]",
898
+ "startup_timeout_sec = 120",
894
899
  'command = "npx"',
895
900
  'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
896
901
  "",
897
902
  "[mcp_servers.playwright]",
903
+ "startup_timeout_sec = 120",
898
904
  'command = "npx"',
899
905
  'args = ["-y", "@playwright/mcp@latest"]',
900
906
  "",
@@ -914,6 +920,44 @@ function hasNamedMcpServer(toml, name) {
914
920
  );
915
921
  }
916
922
 
923
+ function ensureMcpStartupTimeout(toml, name, timeoutSec = 120) {
924
+ const header = `[mcp_servers.${name}]`;
925
+ const headerIdx = toml.indexOf(header);
926
+ if (headerIdx === -1) return { toml, changed: false };
927
+
928
+ const afterHeader = headerIdx + header.length;
929
+ const nextSection = toml.indexOf("\n[", afterHeader);
930
+ const sectionEnd = nextSection === -1 ? toml.length : nextSection;
931
+ let section = toml.substring(afterHeader, sectionEnd);
932
+
933
+ const timeoutRegex = /^startup_timeout_sec\s*=\s*\d+.*$/m;
934
+ let changed = false;
935
+ if (timeoutRegex.test(section)) {
936
+ const desired = `startup_timeout_sec = ${timeoutSec}`;
937
+ const updated = section.replace(timeoutRegex, desired);
938
+ if (updated !== section) {
939
+ section = updated;
940
+ changed = true;
941
+ }
942
+ } else {
943
+ section = section.trimEnd() + `\nstartup_timeout_sec = ${timeoutSec}\n`;
944
+ changed = true;
945
+ }
946
+
947
+ if (!changed) return { toml, changed: false };
948
+ return {
949
+ toml: toml.substring(0, afterHeader) + section + toml.substring(sectionEnd),
950
+ changed: true,
951
+ };
952
+ }
953
+
954
+ function stripDeprecatedSandboxPermissions(toml) {
955
+ return String(toml || "").replace(
956
+ /^\s*sandbox_permissions\s*=.*(?:\r?\n)?/gim,
957
+ "",
958
+ );
959
+ }
960
+
917
961
  // ── Public API ───────────────────────────────────────────────────────────────
918
962
 
919
963
  /**
@@ -1264,13 +1308,15 @@ export function ensureRetrySettings(toml, providerName) {
1264
1308
  * @param {object} opts
1265
1309
  * @param {string} [opts.vkBaseUrl]
1266
1310
  * @param {boolean} [opts.skipVk]
1311
+ * @param {boolean} [opts.manageVkMcp] Explicit opt-in to manage VK MCP in global config
1267
1312
  * @param {boolean} [opts.dryRun] If true, returns result without writing
1268
1313
  * @param {object} [opts.env] Environment overrides (defaults to process.env)
1269
1314
  * @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
1270
1315
  */
1271
1316
  export function ensureCodexConfig({
1272
1317
  vkBaseUrl = "http://127.0.0.1:54089",
1273
- skipVk = false,
1318
+ skipVk = true,
1319
+ manageVkMcp = false,
1274
1320
  dryRun = false,
1275
1321
  env = process.env,
1276
1322
  primarySdk,
@@ -1298,6 +1344,208 @@ export function ensureCodexConfig({
1298
1344
  noChanges: true,
1299
1345
  };
1300
1346
 
1347
+ const configExisted = existsSync(CONFIG_PATH);
1348
+ const originalToml = readCodexConfig();
1349
+ let toml = stripDeprecatedSandboxPermissions(originalToml);
1350
+ if (!configExisted) {
1351
+ result.created = true;
1352
+ toml = "";
1353
+ }
1354
+
1355
+ const sandboxModeResult = ensureTopLevelSandboxMode(
1356
+ toml,
1357
+ env.CODEX_SANDBOX_MODE,
1358
+ );
1359
+ toml = sandboxModeResult.toml;
1360
+ if (sandboxModeResult.changed) {
1361
+ result.sandboxAdded = true;
1362
+ }
1363
+
1364
+ const repoRoot =
1365
+ env.BOSUN_AGENT_REPO_ROOT ||
1366
+ env.REPO_ROOT ||
1367
+ env.BOSUN_HOME ||
1368
+ process.cwd();
1369
+ const additionalRoots = env.BOSUN_WORKSPACES_DIR
1370
+ ? [env.BOSUN_WORKSPACES_DIR]
1371
+ : [];
1372
+ const sandboxWorkspaceResult = ensureSandboxWorkspaceWrite(toml, {
1373
+ repoRoot,
1374
+ additionalRoots,
1375
+ writableRoots: env.CODEX_SANDBOX_WRITABLE_ROOTS,
1376
+ });
1377
+ toml = sandboxWorkspaceResult.toml;
1378
+ result.sandboxWorkspaceAdded = sandboxWorkspaceResult.added;
1379
+ result.sandboxWorkspaceUpdated =
1380
+ sandboxWorkspaceResult.changed && !sandboxWorkspaceResult.added;
1381
+ result.sandboxWorkspaceRootsAdded = sandboxWorkspaceResult.rootsAdded;
1382
+
1383
+ const pruneResult = pruneStaleSandboxRoots(toml);
1384
+ toml = pruneResult.toml;
1385
+ result.sandboxStaleRootsRemoved = pruneResult.removed;
1386
+
1387
+ if (!hasShellEnvPolicy(toml)) {
1388
+ toml += buildShellEnvPolicy(env.CODEX_SHELL_ENV_POLICY || "all");
1389
+ result.shellEnvAdded = true;
1390
+ }
1391
+
1392
+ const rawPrimary = String(primarySdk || env.PRIMARY_AGENT || "codex")
1393
+ .trim()
1394
+ .toLowerCase();
1395
+ const normalizedPrimary =
1396
+ rawPrimary === "copilot" || rawPrimary.includes("copilot")
1397
+ ? "copilot"
1398
+ : rawPrimary === "claude" || rawPrimary.includes("claude")
1399
+ ? "claude"
1400
+ : rawPrimary === "codex" || rawPrimary.includes("codex")
1401
+ ? "codex"
1402
+ : "codex";
1403
+ if (!hasAgentSdkConfig(toml)) {
1404
+ toml += buildAgentSdkBlock({ primary: normalizedPrimary });
1405
+ result.agentSdkAdded = true;
1406
+ }
1407
+
1408
+ const maxThreads = resolveAgentMaxThreads(env);
1409
+ if (maxThreads.explicit && !maxThreads.value) {
1410
+ result.agentMaxThreadsSkipped = String(maxThreads.raw);
1411
+ } else {
1412
+ const maxThreadsResult = ensureAgentMaxThreads(toml, {
1413
+ maxThreads: maxThreads.value,
1414
+ overwrite: maxThreads.explicit,
1415
+ });
1416
+ toml = maxThreadsResult.toml;
1417
+ if (maxThreadsResult.changed && !maxThreadsResult.skipped) {
1418
+ result.agentMaxThreads = {
1419
+ from: maxThreadsResult.existing,
1420
+ to: maxThreadsResult.applied,
1421
+ explicit: maxThreads.explicit,
1422
+ };
1423
+ } else if (maxThreadsResult.skipped && maxThreads.explicit) {
1424
+ result.agentMaxThreadsSkipped = String(maxThreads.raw);
1425
+ }
1426
+ }
1427
+
1428
+ const featureResult = ensureFeatureFlags(toml, env);
1429
+ result.featuresAdded = featureResult.added;
1430
+ toml = featureResult.toml;
1431
+
1432
+ const shouldManageGlobalVkMcp = Boolean(manageVkMcp) && !skipVk;
1433
+ if (!shouldManageGlobalVkMcp) {
1434
+ if (hasVibeKanbanMcp(toml)) {
1435
+ toml = removeVibeKanbanMcp(toml);
1436
+ result.vkRemoved = true;
1437
+ }
1438
+ } else if (!hasVibeKanbanMcp(toml)) {
1439
+ toml += buildVibeKanbanBlock({ vkBaseUrl });
1440
+ result.vkAdded = true;
1441
+ } else {
1442
+ const vkEnvValues = {
1443
+ VK_BASE_URL: vkBaseUrl,
1444
+ VK_ENDPOINT_URL: vkBaseUrl,
1445
+ };
1446
+ const beforeVkEnv = toml;
1447
+ if (!hasVibeKanbanEnv(toml)) {
1448
+ toml =
1449
+ toml.trimEnd() +
1450
+ "\n\n[mcp_servers.vibe_kanban.env]\n" +
1451
+ `VK_BASE_URL = "${vkBaseUrl}"\n` +
1452
+ `VK_ENDPOINT_URL = "${vkBaseUrl}"\n`;
1453
+ } else {
1454
+ toml = updateVibeKanbanEnv(toml, vkEnvValues);
1455
+ }
1456
+ if (toml !== beforeVkEnv) {
1457
+ result.vkEnvUpdated = true;
1458
+ }
1459
+ }
1460
+
1461
+ const commonMcpBlocks = [
1462
+ {
1463
+ present: hasContext7Mcp(toml),
1464
+ block: [
1465
+ "",
1466
+ "# ── Common MCP servers (added by bosun) ──",
1467
+ "[mcp_servers.context7]",
1468
+ "startup_timeout_sec = 120",
1469
+ 'command = "npx"',
1470
+ 'args = ["-y", "@upstash/context7-mcp"]',
1471
+ "",
1472
+ ].join("\n"),
1473
+ },
1474
+ {
1475
+ present: hasNamedMcpServer(toml, "sequential-thinking"),
1476
+ block: [
1477
+ "",
1478
+ "[mcp_servers.sequential-thinking]",
1479
+ "startup_timeout_sec = 120",
1480
+ 'command = "npx"',
1481
+ 'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
1482
+ "",
1483
+ ].join("\n"),
1484
+ },
1485
+ {
1486
+ present: hasNamedMcpServer(toml, "playwright"),
1487
+ block: [
1488
+ "",
1489
+ "[mcp_servers.playwright]",
1490
+ "startup_timeout_sec = 120",
1491
+ 'command = "npx"',
1492
+ 'args = ["-y", "@playwright/mcp@latest"]',
1493
+ "",
1494
+ ].join("\n"),
1495
+ },
1496
+ {
1497
+ present: hasMicrosoftDocsMcp(toml),
1498
+ block: [
1499
+ "",
1500
+ "[mcp_servers.microsoft-docs]",
1501
+ 'url = "https://learn.microsoft.com/api/mcp"',
1502
+ 'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
1503
+ "",
1504
+ ].join("\n"),
1505
+ },
1506
+ ];
1507
+ for (const item of commonMcpBlocks) {
1508
+ if (item.present) continue;
1509
+ toml += item.block;
1510
+ result.commonMcpAdded = true;
1511
+ }
1512
+
1513
+ for (const serverName of ["context7", "sequential-thinking", "playwright"]) {
1514
+ const timeoutResult = ensureMcpStartupTimeout(toml, serverName, 120);
1515
+ toml = timeoutResult.toml;
1516
+ }
1517
+
1518
+ const providerResult = ensureModelProviderSectionsFromEnv(toml, env);
1519
+ toml = providerResult.toml;
1520
+ result.profileProvidersAdded = providerResult.added;
1521
+
1522
+ const timeoutAudit = auditStreamTimeouts(toml);
1523
+ for (const item of timeoutAudit) {
1524
+ if (!item.needsUpdate) continue;
1525
+ toml = setStreamTimeout(toml, item.provider, RECOMMENDED_STREAM_IDLE_TIMEOUT_MS);
1526
+ result.timeoutsFixed.push({
1527
+ provider: item.provider,
1528
+ from: item.currentValue,
1529
+ to: RECOMMENDED_STREAM_IDLE_TIMEOUT_MS,
1530
+ });
1531
+ }
1532
+
1533
+ const providers = auditStreamTimeouts(toml).map((item) => item.provider);
1534
+ for (const provider of providers) {
1535
+ const beforeRetry = toml;
1536
+ toml = ensureRetrySettings(toml, provider);
1537
+ if (toml !== beforeRetry) {
1538
+ result.retriesAdded.push(provider);
1539
+ }
1540
+ }
1541
+
1542
+ const changed = toml !== originalToml;
1543
+ result.noChanges = !result.created && !changed;
1544
+
1545
+ if (!dryRun && (result.created || changed)) {
1546
+ writeCodexConfig(toml);
1547
+ }
1548
+
1301
1549
  return result;
1302
1550
  }
1303
1551
 
@@ -1322,7 +1570,7 @@ export function printConfigSummary(result, log = console.log) {
1322
1570
  }
1323
1571
 
1324
1572
  if (result.vkRemoved) {
1325
- log(" 🗑️ Removed Vibe-Kanban MCP server (VK backend not active)");
1573
+ log(" 🗑️ Removed Vibe-Kanban MCP server from global config (workspace-scoped only)");
1326
1574
  }
1327
1575
 
1328
1576
  if (result.vkEnvUpdated) {
package/config.mjs CHANGED
@@ -29,6 +29,7 @@ import { applyAllCompatibility } from "./compat.mjs";
29
29
  import {
30
30
  normalizeExecutorKey,
31
31
  getModelsForExecutor,
32
+ MODEL_ALIASES,
32
33
  } from "./task-complexity.mjs";
33
34
 
34
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -408,13 +409,43 @@ function parseListValue(value) {
408
409
  .filter(Boolean);
409
410
  }
410
411
 
411
- function normalizeExecutorModels(executor, models) {
412
+ function inferExecutorModelsFromVariant(executor, variant) {
413
+ const normalizedExecutor = normalizeExecutorKey(executor);
414
+ if (!normalizedExecutor) return [];
415
+ const normalizedVariant = String(variant || "DEFAULT")
416
+ .trim()
417
+ .toUpperCase();
418
+ if (!normalizedVariant || normalizedVariant === "DEFAULT") return [];
419
+
420
+ const known = getModelsForExecutor(normalizedExecutor);
421
+ const inferred = known.filter((model) => {
422
+ const alias = MODEL_ALIASES[model];
423
+ return (
424
+ String(alias?.variant || "")
425
+ .trim()
426
+ .toUpperCase() === normalizedVariant
427
+ );
428
+ });
429
+ if (inferred.length > 0) return inferred;
430
+
431
+ // Fallback for variants encoded as model slug with underscores.
432
+ const slugGuess = normalizedVariant.toLowerCase().replaceAll("_", "-");
433
+ if (known.includes(slugGuess)) return [slugGuess];
434
+
435
+ return [];
436
+ }
437
+
438
+ function normalizeExecutorModels(executor, models, variant = "DEFAULT") {
412
439
  const normalizedExecutor = normalizeExecutorKey(executor);
413
440
  if (!normalizedExecutor) return [];
414
441
  const input = parseListValue(models);
415
442
  const known = new Set(getModelsForExecutor(normalizedExecutor));
416
443
  if (input.length === 0) {
417
- return [...known];
444
+ const inferred = inferExecutorModelsFromVariant(
445
+ normalizedExecutor,
446
+ variant,
447
+ );
448
+ return inferred.length > 0 ? inferred : [...known];
418
449
  }
419
450
  return input.filter((model) => known.has(model));
420
451
  }
@@ -433,7 +464,7 @@ function normalizeExecutorEntry(entry, index = 0, total = 1) {
433
464
  const name =
434
465
  String(entry.name || "").trim() ||
435
466
  `${normalized}-${String(variant || "default").toLowerCase()}`;
436
- const models = normalizeExecutorModels(executorType, entry.models);
467
+ const models = normalizeExecutorModels(executorType, entry.models, variant);
437
468
  const codexProfile = String(
438
469
  entry.codexProfile || entry.modelProfile || "",
439
470
  ).trim();
@@ -723,7 +754,11 @@ function parseExecutorsFromEnv() {
723
754
  const parts = entries[i].split(":");
724
755
  if (parts.length < 2) continue;
725
756
  const executorType = parts[0].toUpperCase();
726
- const models = normalizeExecutorModels(executorType, parts[3] || "");
757
+ const models = normalizeExecutorModels(
758
+ executorType,
759
+ parts[3] || "",
760
+ parts[1] || "DEFAULT",
761
+ );
727
762
  executors.push({
728
763
  name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
729
764
  executor: executorType,
@@ -857,12 +892,23 @@ function loadExecutorConfig(configDir, configData) {
857
892
 
858
893
  for (let index = 0; index < executors.length; index++) {
859
894
  const current = executors[index];
860
- if (current.codexProfile) continue;
861
895
  const match = findExecutorMetadataMatch(current, fileExecutors, index);
862
- if (!match?.codexProfile) continue;
896
+ if (!match) continue;
897
+ const merged = { ...current };
898
+ if (typeof match.name === "string" && match.name.trim()) {
899
+ merged.name = match.name.trim();
900
+ }
901
+ if (typeof match.enabled === "boolean") {
902
+ merged.enabled = match.enabled;
903
+ }
904
+ if (Array.isArray(match.models) && match.models.length > 0) {
905
+ merged.models = [...new Set(match.models)];
906
+ }
907
+ if (match.codexProfile) {
908
+ merged.codexProfile = match.codexProfile;
909
+ }
863
910
  executors[index] = {
864
- ...current,
865
- codexProfile: match.codexProfile,
911
+ ...merged,
866
912
  };
867
913
  }
868
914
  }