bosun 0.34.4 → 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.
@@ -30,7 +30,7 @@ const TAG = "[agent-endpoint]";
30
30
  const DEFAULT_PORT = 18432;
31
31
  const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
32
32
  const REQUEST_TIMEOUT_MS = 30_000; // 30 seconds
33
- const ACCESS_DENIED_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
33
+ const ACCESS_DENIED_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
34
34
  const BOSUN_ROOT_HINT = __dirname.toLowerCase().replace(/\\/g, '/');
35
35
 
36
36
  // Valid status transitions when an agent self-reports
@@ -1387,4 +1387,4 @@ export class AgentEndpoint {
1387
1387
  export function createAgentEndpoint(options) {
1388
1388
  return new AgentEndpoint(options);
1389
1389
  }
1390
-
1390
+
@@ -112,8 +112,8 @@ export class AgentEventBus {
112
112
  options.staleThresholdMs || DEFAULTS.staleThresholdMs;
113
113
  this._staleCheckIntervalMs =
114
114
  options.staleCheckIntervalMs || DEFAULTS.staleCheckIntervalMs;
115
- this._maxAutoRetries =
116
- options.maxAutoRetries ?? DEFAULTS.maxAutoRetries;
115
+ this._maxAutoRetries =
116
+ options.maxAutoRetries ?? DEFAULTS.maxAutoRetries;
117
117
  this._dedupeWindowMs = options.dedupeWindowMs || DEFAULTS.dedupeWindowMs;
118
118
 
119
119
  /** @type {Array<{type: string, taskId: string, payload: object, ts: number}>} ring buffer */
@@ -203,7 +203,7 @@ export class AgentEventBus {
203
203
 
204
204
  // ── Dedup
205
205
  const key = `${type}:${taskId}`;
206
- const last = this._recentEmits.get(key);
206
+ const last = this._recentEmits.get(key);
207
207
  if (typeof last === "number" && ts - last < this._dedupeWindowMs) return;
208
208
  this._recentEmits.set(key, ts);
209
209
  if (this._recentEmits.size > 200) {
@@ -359,4 +359,4 @@ export function shouldSendWeeklyReport(options = {}) {
359
359
  return true;
360
360
  }
361
361
  return lastSentMs < scheduledThisWeek.getTime();
362
- }
362
+ }
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/maintenance.mjs CHANGED
@@ -888,26 +888,93 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
888
888
  );
889
889
  if (remoteCheck.status !== 0) continue;
890
890
 
891
- // Compare: is local behind?
891
+ // Measure divergence in both directions up front
892
892
  const behindCheck = spawnSync(
893
893
  "git",
894
894
  ["rev-list", "--count", `${branch}..${remoteRef}`],
895
895
  { cwd: repoRoot, encoding: "utf8", timeout: 5000, windowsHide: true },
896
896
  );
897
897
  const behind = parseInt(behindCheck.stdout?.trim(), 10) || 0;
898
- if (behind === 0) continue; // Already up to date
899
898
 
900
- // Check if local has commits not in remote (diverged)
901
899
  const aheadCheck = spawnSync(
902
900
  "git",
903
901
  ["rev-list", "--count", `${remoteRef}..${branch}`],
904
902
  { cwd: repoRoot, encoding: "utf8", timeout: 5000, windowsHide: true },
905
903
  );
906
904
  const ahead = parseInt(aheadCheck.stdout?.trim(), 10) || 0;
907
- if (ahead > 0) {
908
- console.warn(
909
- `[maintenance] local '${branch}' has ${ahead} commit(s) ahead of ${remoteRef} — skipping (diverged)`,
905
+
906
+ if (behind === 0 && ahead === 0) continue; // Already in sync
907
+
908
+ // Local is ahead of remote but not behind — try a plain push
909
+ if (behind === 0 && ahead > 0) {
910
+ const push = spawnSync(
911
+ "git",
912
+ ["push", "origin", `${branch}:refs/heads/${branch}`, "--quiet"],
913
+ { cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
910
914
  );
915
+ if (push.status === 0) {
916
+ console.log(
917
+ `[maintenance] pushed local '${branch}' to origin (${ahead} commit(s) ahead)`,
918
+ );
919
+ synced++;
920
+ } else {
921
+ console.warn(
922
+ `[maintenance] git push '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
923
+ );
924
+ }
925
+ continue;
926
+ }
927
+
928
+ // Truly diverged: local has unique commits AND is missing remote commits.
929
+ // Attempt rebase onto remote then push (checked-out branch only).
930
+ if (ahead > 0) {
931
+ const statusCheck = spawnSync("git", ["status", "--porcelain"], {
932
+ cwd: repoRoot,
933
+ encoding: "utf8",
934
+ timeout: 5000,
935
+ windowsHide: true,
936
+ });
937
+ if (statusCheck.stdout?.trim()) {
938
+ console.warn(
939
+ `[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) but has uncommitted changes — skipping`,
940
+ );
941
+ continue;
942
+ }
943
+ if (branch === currentBranch) {
944
+ const rebase = spawnSync(
945
+ "git",
946
+ ["rebase", remoteRef, "--quiet"],
947
+ { cwd: repoRoot, encoding: "utf8", timeout: 60_000, windowsHide: true },
948
+ );
949
+ if (rebase.status !== 0) {
950
+ spawnSync("git", ["rebase", "--abort"], {
951
+ cwd: repoRoot, timeout: 10_000, windowsHide: true,
952
+ });
953
+ console.warn(
954
+ `[maintenance] rebase of '${branch}' onto ${remoteRef} failed (${ahead}↑ ${behind}↓) — skipping`,
955
+ );
956
+ continue;
957
+ }
958
+ const push = spawnSync(
959
+ "git",
960
+ ["push", "origin", `${branch}:refs/heads/${branch}`, "--quiet"],
961
+ { cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
962
+ );
963
+ if (push.status === 0) {
964
+ console.log(
965
+ `[maintenance] rebased and pushed '${branch}' (was ${ahead}↑ ${behind}↓)`,
966
+ );
967
+ synced++;
968
+ } else {
969
+ console.warn(
970
+ `[maintenance] push after rebase of '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
971
+ );
972
+ }
973
+ } else {
974
+ console.warn(
975
+ `[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) and not checked out — skipping (rebase requires checkout)`,
976
+ );
977
+ }
911
978
  continue;
912
979
  }
913
980