bosun 0.34.5 → 0.34.6

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/codex-config.mjs CHANGED
@@ -887,14 +887,17 @@ export function buildCommonMcpBlocks() {
887
887
  "",
888
888
  "# ── Common MCP servers (added by bosun) ──",
889
889
  "[mcp_servers.context7]",
890
+ "startup_timeout_sec = 120",
890
891
  'command = "npx"',
891
892
  'args = ["-y", "@upstash/context7-mcp"]',
892
893
  "",
893
894
  "[mcp_servers.sequential-thinking]",
895
+ "startup_timeout_sec = 120",
894
896
  'command = "npx"',
895
897
  'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
896
898
  "",
897
899
  "[mcp_servers.playwright]",
900
+ "startup_timeout_sec = 120",
898
901
  'command = "npx"',
899
902
  'args = ["-y", "@playwright/mcp@latest"]',
900
903
  "",
@@ -914,6 +917,44 @@ function hasNamedMcpServer(toml, name) {
914
917
  );
915
918
  }
916
919
 
920
+ function ensureMcpStartupTimeout(toml, name, timeoutSec = 120) {
921
+ const header = `[mcp_servers.${name}]`;
922
+ const headerIdx = toml.indexOf(header);
923
+ if (headerIdx === -1) return { toml, changed: false };
924
+
925
+ const afterHeader = headerIdx + header.length;
926
+ const nextSection = toml.indexOf("\n[", afterHeader);
927
+ const sectionEnd = nextSection === -1 ? toml.length : nextSection;
928
+ let section = toml.substring(afterHeader, sectionEnd);
929
+
930
+ const timeoutRegex = /^startup_timeout_sec\s*=\s*\d+.*$/m;
931
+ let changed = false;
932
+ if (timeoutRegex.test(section)) {
933
+ const desired = `startup_timeout_sec = ${timeoutSec}`;
934
+ const updated = section.replace(timeoutRegex, desired);
935
+ if (updated !== section) {
936
+ section = updated;
937
+ changed = true;
938
+ }
939
+ } else {
940
+ section = section.trimEnd() + `\nstartup_timeout_sec = ${timeoutSec}\n`;
941
+ changed = true;
942
+ }
943
+
944
+ if (!changed) return { toml, changed: false };
945
+ return {
946
+ toml: toml.substring(0, afterHeader) + section + toml.substring(sectionEnd),
947
+ changed: true,
948
+ };
949
+ }
950
+
951
+ function stripDeprecatedSandboxPermissions(toml) {
952
+ return String(toml || "").replace(
953
+ /^\s*sandbox_permissions\s*=.*(?:\r?\n)?/gim,
954
+ "",
955
+ );
956
+ }
957
+
917
958
  // ── Public API ───────────────────────────────────────────────────────────────
918
959
 
919
960
  /**
@@ -1298,6 +1339,207 @@ export function ensureCodexConfig({
1298
1339
  noChanges: true,
1299
1340
  };
1300
1341
 
1342
+ const configExisted = existsSync(CONFIG_PATH);
1343
+ const originalToml = readCodexConfig();
1344
+ let toml = stripDeprecatedSandboxPermissions(originalToml);
1345
+ if (!configExisted) {
1346
+ result.created = true;
1347
+ toml = "";
1348
+ }
1349
+
1350
+ const sandboxModeResult = ensureTopLevelSandboxMode(
1351
+ toml,
1352
+ env.CODEX_SANDBOX_MODE,
1353
+ );
1354
+ toml = sandboxModeResult.toml;
1355
+ if (sandboxModeResult.changed) {
1356
+ result.sandboxAdded = true;
1357
+ }
1358
+
1359
+ const repoRoot =
1360
+ env.BOSUN_AGENT_REPO_ROOT ||
1361
+ env.REPO_ROOT ||
1362
+ env.BOSUN_HOME ||
1363
+ process.cwd();
1364
+ const additionalRoots = env.BOSUN_WORKSPACES_DIR
1365
+ ? [env.BOSUN_WORKSPACES_DIR]
1366
+ : [];
1367
+ const sandboxWorkspaceResult = ensureSandboxWorkspaceWrite(toml, {
1368
+ repoRoot,
1369
+ additionalRoots,
1370
+ writableRoots: env.CODEX_SANDBOX_WRITABLE_ROOTS,
1371
+ });
1372
+ toml = sandboxWorkspaceResult.toml;
1373
+ result.sandboxWorkspaceAdded = sandboxWorkspaceResult.added;
1374
+ result.sandboxWorkspaceUpdated =
1375
+ sandboxWorkspaceResult.changed && !sandboxWorkspaceResult.added;
1376
+ result.sandboxWorkspaceRootsAdded = sandboxWorkspaceResult.rootsAdded;
1377
+
1378
+ const pruneResult = pruneStaleSandboxRoots(toml);
1379
+ toml = pruneResult.toml;
1380
+ result.sandboxStaleRootsRemoved = pruneResult.removed;
1381
+
1382
+ if (!hasShellEnvPolicy(toml)) {
1383
+ toml += buildShellEnvPolicy(env.CODEX_SHELL_ENV_POLICY || "all");
1384
+ result.shellEnvAdded = true;
1385
+ }
1386
+
1387
+ const rawPrimary = String(primarySdk || env.PRIMARY_AGENT || "codex")
1388
+ .trim()
1389
+ .toLowerCase();
1390
+ const normalizedPrimary =
1391
+ rawPrimary === "copilot" || rawPrimary.includes("copilot")
1392
+ ? "copilot"
1393
+ : rawPrimary === "claude" || rawPrimary.includes("claude")
1394
+ ? "claude"
1395
+ : rawPrimary === "codex" || rawPrimary.includes("codex")
1396
+ ? "codex"
1397
+ : "codex";
1398
+ if (!hasAgentSdkConfig(toml)) {
1399
+ toml += buildAgentSdkBlock({ primary: normalizedPrimary });
1400
+ result.agentSdkAdded = true;
1401
+ }
1402
+
1403
+ const maxThreads = resolveAgentMaxThreads(env);
1404
+ if (maxThreads.explicit && !maxThreads.value) {
1405
+ result.agentMaxThreadsSkipped = String(maxThreads.raw);
1406
+ } else {
1407
+ const maxThreadsResult = ensureAgentMaxThreads(toml, {
1408
+ maxThreads: maxThreads.value,
1409
+ overwrite: maxThreads.explicit,
1410
+ });
1411
+ toml = maxThreadsResult.toml;
1412
+ if (maxThreadsResult.changed && !maxThreadsResult.skipped) {
1413
+ result.agentMaxThreads = {
1414
+ from: maxThreadsResult.existing,
1415
+ to: maxThreadsResult.applied,
1416
+ explicit: maxThreads.explicit,
1417
+ };
1418
+ } else if (maxThreadsResult.skipped && maxThreads.explicit) {
1419
+ result.agentMaxThreadsSkipped = String(maxThreads.raw);
1420
+ }
1421
+ }
1422
+
1423
+ const featureResult = ensureFeatureFlags(toml, env);
1424
+ result.featuresAdded = featureResult.added;
1425
+ toml = featureResult.toml;
1426
+
1427
+ if (skipVk) {
1428
+ if (hasVibeKanbanMcp(toml)) {
1429
+ toml = removeVibeKanbanMcp(toml);
1430
+ result.vkRemoved = true;
1431
+ }
1432
+ } else if (!hasVibeKanbanMcp(toml)) {
1433
+ toml += buildVibeKanbanBlock({ vkBaseUrl });
1434
+ result.vkAdded = true;
1435
+ } else {
1436
+ const vkEnvValues = {
1437
+ VK_BASE_URL: vkBaseUrl,
1438
+ VK_ENDPOINT_URL: vkBaseUrl,
1439
+ };
1440
+ const beforeVkEnv = toml;
1441
+ if (!hasVibeKanbanEnv(toml)) {
1442
+ toml =
1443
+ toml.trimEnd() +
1444
+ "\n\n[mcp_servers.vibe_kanban.env]\n" +
1445
+ `VK_BASE_URL = "${vkBaseUrl}"\n` +
1446
+ `VK_ENDPOINT_URL = "${vkBaseUrl}"\n`;
1447
+ } else {
1448
+ toml = updateVibeKanbanEnv(toml, vkEnvValues);
1449
+ }
1450
+ if (toml !== beforeVkEnv) {
1451
+ result.vkEnvUpdated = true;
1452
+ }
1453
+ }
1454
+
1455
+ const commonMcpBlocks = [
1456
+ {
1457
+ present: hasContext7Mcp(toml),
1458
+ block: [
1459
+ "",
1460
+ "# ── Common MCP servers (added by bosun) ──",
1461
+ "[mcp_servers.context7]",
1462
+ "startup_timeout_sec = 120",
1463
+ 'command = "npx"',
1464
+ 'args = ["-y", "@upstash/context7-mcp"]',
1465
+ "",
1466
+ ].join("\n"),
1467
+ },
1468
+ {
1469
+ present: hasNamedMcpServer(toml, "sequential-thinking"),
1470
+ block: [
1471
+ "",
1472
+ "[mcp_servers.sequential-thinking]",
1473
+ "startup_timeout_sec = 120",
1474
+ 'command = "npx"',
1475
+ 'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
1476
+ "",
1477
+ ].join("\n"),
1478
+ },
1479
+ {
1480
+ present: hasNamedMcpServer(toml, "playwright"),
1481
+ block: [
1482
+ "",
1483
+ "[mcp_servers.playwright]",
1484
+ "startup_timeout_sec = 120",
1485
+ 'command = "npx"',
1486
+ 'args = ["-y", "@playwright/mcp@latest"]',
1487
+ "",
1488
+ ].join("\n"),
1489
+ },
1490
+ {
1491
+ present: hasMicrosoftDocsMcp(toml),
1492
+ block: [
1493
+ "",
1494
+ "[mcp_servers.microsoft-docs]",
1495
+ 'url = "https://learn.microsoft.com/api/mcp"',
1496
+ 'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
1497
+ "",
1498
+ ].join("\n"),
1499
+ },
1500
+ ];
1501
+ for (const item of commonMcpBlocks) {
1502
+ if (item.present) continue;
1503
+ toml += item.block;
1504
+ result.commonMcpAdded = true;
1505
+ }
1506
+
1507
+ for (const serverName of ["context7", "sequential-thinking", "playwright"]) {
1508
+ const timeoutResult = ensureMcpStartupTimeout(toml, serverName, 120);
1509
+ toml = timeoutResult.toml;
1510
+ }
1511
+
1512
+ const providerResult = ensureModelProviderSectionsFromEnv(toml, env);
1513
+ toml = providerResult.toml;
1514
+ result.profileProvidersAdded = providerResult.added;
1515
+
1516
+ const timeoutAudit = auditStreamTimeouts(toml);
1517
+ for (const item of timeoutAudit) {
1518
+ if (!item.needsUpdate) continue;
1519
+ toml = setStreamTimeout(toml, item.provider, RECOMMENDED_STREAM_IDLE_TIMEOUT_MS);
1520
+ result.timeoutsFixed.push({
1521
+ provider: item.provider,
1522
+ from: item.currentValue,
1523
+ to: RECOMMENDED_STREAM_IDLE_TIMEOUT_MS,
1524
+ });
1525
+ }
1526
+
1527
+ const providers = auditStreamTimeouts(toml).map((item) => item.provider);
1528
+ for (const provider of providers) {
1529
+ const beforeRetry = toml;
1530
+ toml = ensureRetrySettings(toml, provider);
1531
+ if (toml !== beforeRetry) {
1532
+ result.retriesAdded.push(provider);
1533
+ }
1534
+ }
1535
+
1536
+ const changed = toml !== originalToml;
1537
+ result.noChanges = !result.created && !changed;
1538
+
1539
+ if (!dryRun && (result.created || changed)) {
1540
+ writeCodexConfig(toml);
1541
+ }
1542
+
1301
1543
  return result;
1302
1544
  }
1303
1545
 
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
  }
package/monitor.mjs CHANGED
@@ -478,8 +478,12 @@ async function ensureWorkflowAutomationEngine() {
478
478
  delete payload.projectId;
479
479
  return createTask(projectId, payload);
480
480
  },
481
- updateTaskStatus: async (taskId, status) =>
482
- updateKanbanTaskStatus(String(taskId || ""), String(status || "")),
481
+ updateTaskStatus: async (taskId, status, options = {}) =>
482
+ updateTaskStatus(
483
+ String(taskId || ""),
484
+ String(status || ""),
485
+ options && typeof options === "object" ? options : {},
486
+ ),
483
487
  listTasks: async (projectId, filters = {}) =>
484
488
  listKanbanTasks(String(projectId || ""), filters || {}),
485
489
  };
@@ -4431,6 +4435,10 @@ async function updateTaskStatus(taskId, newStatus, options = {}) {
4431
4435
  options?.taskData && typeof options.taskData === "object"
4432
4436
  ? options.taskData
4433
4437
  : null;
4438
+ const workflowData =
4439
+ options?.workflowData && typeof options.workflowData === "object"
4440
+ ? options.workflowData
4441
+ : null;
4434
4442
  const taskTitle = String(
4435
4443
  options?.taskTitle || taskData?.title || getInternalTask(normalizedTaskId)?.title || "",
4436
4444
  ).trim();
@@ -4444,6 +4452,43 @@ async function updateTaskStatus(taskId, newStatus, options = {}) {
4444
4452
  taskTitle,
4445
4453
  previousStatus: previousStatus || null,
4446
4454
  status: normalizedStatus,
4455
+ branch: String(
4456
+ options?.branch ||
4457
+ taskData?.branch ||
4458
+ taskData?.branchName ||
4459
+ taskData?.meta?.branch_name ||
4460
+ workflowData?.branch ||
4461
+ "",
4462
+ ).trim() || null,
4463
+ baseBranch: String(
4464
+ options?.baseBranch ||
4465
+ taskData?.baseBranch ||
4466
+ taskData?.base_branch ||
4467
+ taskData?.meta?.base_branch ||
4468
+ workflowData?.baseBranch ||
4469
+ "",
4470
+ ).trim() || null,
4471
+ worktreePath: String(
4472
+ options?.worktreePath ||
4473
+ taskData?.worktreePath ||
4474
+ taskData?.meta?.worktreePath ||
4475
+ workflowData?.worktreePath ||
4476
+ "",
4477
+ ).trim() || null,
4478
+ prNumber: String(
4479
+ options?.prNumber ||
4480
+ taskData?.prNumber ||
4481
+ taskData?.meta?.pr_number ||
4482
+ workflowData?.prNumber ||
4483
+ "",
4484
+ ).trim() || null,
4485
+ prUrl: String(
4486
+ options?.prUrl ||
4487
+ taskData?.prUrl ||
4488
+ taskData?.meta?.pr_url ||
4489
+ workflowData?.prUrl ||
4490
+ "",
4491
+ ).trim() || null,
4447
4492
  };
4448
4493
  const queueTaskStatusWorkflowEvents = () => {
4449
4494
  const statusChanged = previousStatus !== normalizedStatus;
@@ -14536,6 +14581,29 @@ if (isExecutorDisabled()) {
14536
14581
  },
14537
14582
  onTaskCompleted: (task, result) => {
14538
14583
  const taskId = String(task?.id || task?.task_id || "").trim();
14584
+ const branch = String(
14585
+ result?.branch ||
14586
+ task?.branchName ||
14587
+ task?.meta?.branch_name ||
14588
+ "",
14589
+ ).trim() || null;
14590
+ const worktreePath = String(
14591
+ result?.worktreePath ||
14592
+ task?.worktreePath ||
14593
+ task?.meta?.worktreePath ||
14594
+ "",
14595
+ ).trim() || null;
14596
+ const prNumber = result?.prNumber
14597
+ ? String(result.prNumber)
14598
+ : null;
14599
+ const prUrl = String(result?.prUrl || "").trim() || null;
14600
+ const baseBranch = String(
14601
+ result?.baseBranch ||
14602
+ task?.baseBranch ||
14603
+ task?.base_branch ||
14604
+ task?.meta?.base_branch ||
14605
+ "",
14606
+ ).trim() || null;
14539
14607
  console.log(
14540
14608
  `[task-executor] ✅ completed: "${task.title}" (${result.attempts} attempt(s))`,
14541
14609
  );
@@ -14566,6 +14634,11 @@ if (isExecutorDisabled()) {
14566
14634
  taskStatus: "completed",
14567
14635
  attempts: Number(result?.attempts || 0),
14568
14636
  success: result?.success !== false,
14637
+ branch,
14638
+ worktreePath,
14639
+ prNumber,
14640
+ prUrl,
14641
+ baseBranch,
14569
14642
  },
14570
14643
  { dedupKey: `workflow-event:task.completed:${taskId}:${result?.attempts || 0}` },
14571
14644
  );
@@ -14573,6 +14646,27 @@ if (isExecutorDisabled()) {
14573
14646
  },
14574
14647
  onTaskFailed: (task, err) => {
14575
14648
  const taskId = String(task?.id || task?.task_id || "").trim();
14649
+ const branch = String(
14650
+ err?.branch ||
14651
+ task?.branchName ||
14652
+ task?.meta?.branch_name ||
14653
+ "",
14654
+ ).trim() || null;
14655
+ const worktreePath = String(
14656
+ err?.worktreePath ||
14657
+ task?.worktreePath ||
14658
+ task?.meta?.worktreePath ||
14659
+ "",
14660
+ ).trim() || null;
14661
+ const baseBranch = String(
14662
+ err?.baseBranch ||
14663
+ task?.baseBranch ||
14664
+ task?.base_branch ||
14665
+ task?.meta?.base_branch ||
14666
+ "",
14667
+ ).trim() || null;
14668
+ const attempts =
14669
+ Number(err?.attempts || 0) > 0 ? Number(err.attempts) : null;
14576
14670
  console.warn(
14577
14671
  `[task-executor] ❌ failed: "${task.title}" — ${formatMonitorError(err)}`,
14578
14672
  );
@@ -14586,6 +14680,10 @@ if (isExecutorDisabled()) {
14586
14680
  taskTitle: task?.title || "",
14587
14681
  taskStatus: "failed",
14588
14682
  error: errorMessage,
14683
+ branch,
14684
+ worktreePath,
14685
+ baseBranch,
14686
+ attempts,
14589
14687
  },
14590
14688
  { dedupKey: `workflow-event:task.failed:${taskId}:${errorMessage}` },
14591
14689
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.34.5",
3
+ "version": "0.34.6",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",