bosun 0.34.4 → 0.34.5

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/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
 
package/monitor.mjs CHANGED
@@ -678,6 +678,23 @@ async function pollAgentAlerts() {
678
678
  }),
679
679
  );
680
680
  }
681
+
682
+ // Act on high-error alerts: apply a cooldown so the task-executor does not
683
+ // immediately restart the same session against an API that is failing.
684
+ if (alert.type === "failed_session_high_errors" && alert.task_id && internalTaskExecutor) {
685
+ try {
686
+ const taskId = alert.task_id;
687
+ const cooldownUntil = Date.now() + 15 * 60_000; // 15-minute cooldown
688
+ if (typeof internalTaskExecutor.applyTaskCooldown === "function") {
689
+ internalTaskExecutor.applyTaskCooldown(taskId, cooldownUntil);
690
+ } else if (internalTaskExecutor._skipUntil instanceof Map) {
691
+ internalTaskExecutor._skipUntil.set(taskId, cooldownUntil);
692
+ }
693
+ console.warn(
694
+ `[monitor] 15m cooldown applied to task ${taskId} after ${alert.error_count || "?"} API errors (executor: ${alert.executor || "unknown"})`,
695
+ );
696
+ } catch { /* best effort */ }
697
+ }
681
698
  }
682
699
  saveAgentAlertsState();
683
700
  }
@@ -1500,11 +1517,11 @@ const ALLOW_INTERNAL_RUNTIME_RESTARTS = isTruthyFlag(
1500
1517
  );
1501
1518
  const SELF_RESTART_DEFER_HARD_CAP = Math.max(
1502
1519
  1,
1503
- Number(process.env.SELF_RESTART_DEFER_HARD_CAP || "20") || 20,
1520
+ Number(process.env.SELF_RESTART_DEFER_HARD_CAP || "6") || 6,
1504
1521
  );
1505
1522
  const SELF_RESTART_MAX_DEFER_MS = Math.max(
1506
1523
  60_000,
1507
- Number(process.env.SELF_RESTART_MAX_DEFER_MS || "600000") || 600000,
1524
+ Number(process.env.SELF_RESTART_MAX_DEFER_MS || "180000") || 180000,
1508
1525
  );
1509
1526
  const SELF_RESTART_FORCE_ACTIVE_SLOT_MIN_AGE_MS = Math.max(
1510
1527
  60_000,
@@ -4784,12 +4801,39 @@ async function isBranchMerged(branch, baseBranch) {
4784
4801
  const baseRef = `${baseInfo.remote}/${baseInfo.name}`;
4785
4802
  const ghHead = branchInfo.name || branch;
4786
4803
 
4787
- // ── Strategy 1: Check GitHub for a merged PR with this head branch ──
4788
- // This is the most reliable signal if GitHub says merged, it's merged.
4804
+ // ── Strategy 1: Check GitHub PR state for this head branch ───────────
4805
+ // Open PR always wins over historical merged PRs for the same head.
4806
+ // A branch can have an old merged PR and a newer open PR with fresh commits.
4807
+ // In that case, treat as NOT merged.
4789
4808
  if (ghAvailable()) {
4809
+ try {
4810
+ const openResult = execSync(
4811
+ `gh pr list --head "${ghHead}" --state open --json number,baseRefName --limit 10`,
4812
+ {
4813
+ cwd: repoRoot,
4814
+ encoding: "utf8",
4815
+ stdio: ["pipe", "pipe", "ignore"],
4816
+ timeout: 15000,
4817
+ },
4818
+ ).trim();
4819
+ const openPRs = JSON.parse(openResult || "[]");
4820
+ const hasOpenForTarget = openPRs.some((pr) => {
4821
+ const prBase = normalizeBranchName(pr?.baseRefName);
4822
+ return !prBase || prBase === baseInfo.name;
4823
+ });
4824
+ if (hasOpenForTarget || openPRs.length > 0) {
4825
+ console.log(
4826
+ `[monitor] Branch ${branch} has open PR(s) — treating as NOT merged`,
4827
+ );
4828
+ return false;
4829
+ }
4830
+ } catch {
4831
+ // best-effort
4832
+ }
4833
+
4790
4834
  try {
4791
4835
  const ghResult = execSync(
4792
- `gh pr list --head "${ghHead}" --state merged --json number,mergedAt --limit 1`,
4836
+ `gh pr list --head "${ghHead}" --base "${baseInfo.name}" --state merged --json number,mergedAt --limit 1`,
4793
4837
  {
4794
4838
  cwd: repoRoot,
4795
4839
  encoding: "utf8",
@@ -4805,7 +4849,27 @@ async function isBranchMerged(branch, baseBranch) {
4805
4849
  return true;
4806
4850
  }
4807
4851
  } catch {
4808
- // gh failed fall through to git-based checks
4852
+ // Fallback for older gh variants / edge cases that reject --base here.
4853
+ try {
4854
+ const ghResult = execSync(
4855
+ `gh pr list --head "${ghHead}" --state merged --json number,mergedAt --limit 1`,
4856
+ {
4857
+ cwd: repoRoot,
4858
+ encoding: "utf8",
4859
+ stdio: ["pipe", "pipe", "ignore"],
4860
+ timeout: 15000,
4861
+ },
4862
+ ).trim();
4863
+ const mergedPRs = JSON.parse(ghResult || "[]");
4864
+ if (mergedPRs.length > 0) {
4865
+ console.log(
4866
+ `[monitor] Branch ${branch} has merged PR #${mergedPRs[0].number}`,
4867
+ );
4868
+ return true;
4869
+ }
4870
+ } catch {
4871
+ // gh failed — fall through to git-based checks
4872
+ }
4809
4873
  }
4810
4874
  }
4811
4875
 
@@ -4873,6 +4937,45 @@ const mergedTaskCache = new Set();
4873
4937
  */
4874
4938
  const mergedBranchCache = new Set();
4875
4939
 
4940
+ function normalizeMergedBranchKey(branch) {
4941
+ const normalized = normalizeBranchName(branch);
4942
+ if (normalized) return normalized;
4943
+ const raw = String(branch || "").trim();
4944
+ return raw || "";
4945
+ }
4946
+
4947
+ function addMergedBranchCache(branch) {
4948
+ const key = normalizeMergedBranchKey(branch);
4949
+ if (!key) return false;
4950
+ mergedBranchCache.add(key);
4951
+ return true;
4952
+ }
4953
+
4954
+ function hasMergedBranchCache(branch) {
4955
+ const key = normalizeMergedBranchKey(branch);
4956
+ if (!key) return false;
4957
+ return mergedBranchCache.has(key);
4958
+ }
4959
+
4960
+ function removeMergedBranchCache(branch) {
4961
+ const key = normalizeMergedBranchKey(branch);
4962
+ if (!key) return false;
4963
+ return mergedBranchCache.delete(key);
4964
+ }
4965
+
4966
+ async function isMergedBranchCacheEntryStillValid(branch, baseBranch) {
4967
+ if (!hasMergedBranchCache(branch)) return false;
4968
+ const stillMerged = await isBranchMerged(branch, baseBranch);
4969
+ if (stillMerged) return true;
4970
+ if (removeMergedBranchCache(branch)) {
4971
+ saveMergedTaskCache();
4972
+ console.log(
4973
+ `[monitor] Branch ${branch} removed from merged cache after revalidation`,
4974
+ );
4975
+ }
4976
+ return false;
4977
+ }
4978
+
4876
4979
  /** Path to the persistent merged-task cache file */
4877
4980
  const mergedTaskCachePath = resolve(
4878
4981
  config.cacheDir || resolve(config.repoRoot, ".cache"),
@@ -4892,7 +4995,7 @@ function loadMergedTaskCache() {
4892
4995
  }
4893
4996
  if (Array.isArray(data.branches)) {
4894
4997
  for (const b of data.branches) {
4895
- mergedBranchCache.add(b);
4998
+ addMergedBranchCache(b);
4896
4999
  }
4897
5000
  }
4898
5001
  const total = mergedTaskCache.size + mergedBranchCache.size;
@@ -5236,9 +5339,27 @@ async function checkMergedPRsAndUpdateTasks() {
5236
5339
  }
5237
5340
  }
5238
5341
  });
5239
- const reviewTasks = Array.from(taskMap.values()).filter(
5240
- (entry) => !mergedTaskCache.has(entry.task.id),
5241
- );
5342
+ const reviewTasks = [];
5343
+ let prunedMergedTaskCacheCount = 0;
5344
+ for (const entry of taskMap.values()) {
5345
+ const taskId = String(entry?.task?.id || "").trim();
5346
+ if (!taskId) continue;
5347
+ if (!mergedTaskCache.has(taskId)) {
5348
+ reviewTasks.push(entry);
5349
+ continue;
5350
+ }
5351
+ // Task is active (inprogress/inreview) so a previous done-cache entry
5352
+ // is stale (e.g. task reopened or status rollback) and must be purged.
5353
+ mergedTaskCache.delete(taskId);
5354
+ prunedMergedTaskCacheCount++;
5355
+ reviewTasks.push(entry);
5356
+ }
5357
+ if (prunedMergedTaskCacheCount > 0) {
5358
+ saveMergedTaskCache();
5359
+ console.log(
5360
+ `[monitor] Pruned ${prunedMergedTaskCacheCount} stale merged-task cache entr${prunedMergedTaskCacheCount === 1 ? "y" : "ies"} for active tasks`,
5361
+ );
5362
+ }
5242
5363
  if (reviewTasks.length === 0) {
5243
5364
  console.log(
5244
5365
  "[monitor] No tasks in review/inprogress status (after dedup)",
@@ -5533,34 +5654,40 @@ async function checkMergedPRsAndUpdateTasks() {
5533
5654
 
5534
5655
  // ── Branch-level dedup: skip if ANY branch is already known-merged ──
5535
5656
  const knownBranch = candidates.find(
5536
- (c) => c.branch && mergedBranchCache.has(c.branch),
5657
+ (c) => c.branch && hasMergedBranchCache(c.branch),
5537
5658
  );
5538
- if (knownBranch) {
5539
- const canFinalize = await shouldFinalizeMergedTask(task, {
5540
- branch: knownBranch.branch,
5541
- prNumber: knownBranch.prNumber,
5542
- reason: "known_merged_branch",
5543
- });
5544
- if (!canFinalize) {
5659
+ if (knownBranch?.branch) {
5660
+ const cachedStillMerged = await isMergedBranchCacheEntryStillValid(
5661
+ knownBranch.branch,
5662
+ knownBranch.baseBranch,
5663
+ );
5664
+ if (cachedStillMerged) {
5665
+ const canFinalize = await shouldFinalizeMergedTask(task, {
5666
+ branch: knownBranch.branch,
5667
+ prNumber: knownBranch.prNumber,
5668
+ reason: "known_merged_branch",
5669
+ });
5670
+ if (!canFinalize) {
5671
+ continue;
5672
+ }
5673
+ mergedTaskCache.add(task.id);
5674
+ pendingMergeStrategyByTask.delete(String(task.id || "").trim());
5675
+ // Cache all branches for this task
5676
+ for (const c of candidates) {
5677
+ if (c.branch) addMergedBranchCache(c.branch);
5678
+ }
5679
+ saveMergedTaskCache();
5680
+ void updateTaskStatus(task.id, "done", {
5681
+ taskData: task,
5682
+ workflowEvent: "pr.merged",
5683
+ workflowData: {
5684
+ prNumber: knownBranch.prNumber || null,
5685
+ branch: knownBranch.branch || null,
5686
+ triggerReason: "known_merged_branch",
5687
+ },
5688
+ });
5545
5689
  continue;
5546
5690
  }
5547
- mergedTaskCache.add(task.id);
5548
- pendingMergeStrategyByTask.delete(String(task.id || "").trim());
5549
- // Cache all branches for this task
5550
- for (const c of candidates) {
5551
- if (c.branch) mergedBranchCache.add(c.branch);
5552
- }
5553
- saveMergedTaskCache();
5554
- void updateTaskStatus(task.id, "done", {
5555
- taskData: task,
5556
- workflowEvent: "pr.merged",
5557
- workflowData: {
5558
- prNumber: knownBranch.prNumber || null,
5559
- branch: knownBranch.branch || null,
5560
- triggerReason: "known_merged_branch",
5561
- },
5562
- });
5563
- continue;
5564
5691
  }
5565
5692
 
5566
5693
  // ── Check ALL candidates for a merged PR/branch ──
@@ -5624,7 +5751,7 @@ async function checkMergedPRsAndUpdateTasks() {
5624
5751
  mergedTaskCache.add(task.id);
5625
5752
  pendingMergeStrategyByTask.delete(String(task.id || "").trim());
5626
5753
  for (const c of candidates) {
5627
- if (c.branch) mergedBranchCache.add(c.branch);
5754
+ if (c.branch) addMergedBranchCache(c.branch);
5628
5755
  }
5629
5756
  saveMergedTaskCache();
5630
5757
  completedTaskNames.push(task.title);
@@ -5709,7 +5836,7 @@ async function checkMergedPRsAndUpdateTasks() {
5709
5836
  mergedTaskCache.add(task.id);
5710
5837
  pendingMergeStrategyByTask.delete(String(task.id || "").trim());
5711
5838
  for (const c of candidates) {
5712
- if (c.branch) mergedBranchCache.add(c.branch);
5839
+ if (c.branch) addMergedBranchCache(c.branch);
5713
5840
  }
5714
5841
  saveMergedTaskCache();
5715
5842
  completedTaskNames.push(task.title);
@@ -8018,7 +8145,16 @@ async function smartPRFlow(attemptId, shortId, status) {
8018
8145
  const attemptInfo = await getAttemptInfo(attemptId);
8019
8146
  let taskData = null;
8020
8147
  if (attemptInfo?.branch) {
8021
- if (mergedBranchCache.has(attemptInfo.branch)) {
8148
+ if (
8149
+ await isMergedBranchCacheEntryStillValid(
8150
+ attemptInfo.branch,
8151
+ attemptInfo?.target_branch ||
8152
+ attemptInfo?.targetBranch ||
8153
+ attemptInfo?.base_branch ||
8154
+ attemptInfo?.baseBranch ||
8155
+ null,
8156
+ )
8157
+ ) {
8022
8158
  console.log(
8023
8159
  `[monitor] ${tag}: branch already in merged cache — archiving`,
8024
8160
  );
@@ -8047,7 +8183,7 @@ async function smartPRFlow(attemptId, shortId, status) {
8047
8183
  console.log(
8048
8184
  `[monitor] ${tag}: branch ${attemptInfo.branch} confirmed merged — ${canFinalize ? "completing task" : "awaiting review gate"}`,
8049
8185
  );
8050
- mergedBranchCache.add(attemptInfo.branch);
8186
+ addMergedBranchCache(attemptInfo.branch);
8051
8187
  if (attemptInfo.task_id && canFinalize) {
8052
8188
  mergedTaskCache.add(attemptInfo.task_id);
8053
8189
  pendingMergeStrategyByTask.delete(
@@ -8553,7 +8689,16 @@ async function resolveAndTriggerSmartPR(shortId, status) {
8553
8689
  // ── Early merged-branch check: skip if branch is already merged ──
8554
8690
  const resolvedAttempt = match;
8555
8691
  if (resolvedAttempt?.branch) {
8556
- if (mergedBranchCache.has(resolvedAttempt.branch)) {
8692
+ if (
8693
+ await isMergedBranchCacheEntryStillValid(
8694
+ resolvedAttempt.branch,
8695
+ resolvedAttempt?.target_branch ||
8696
+ resolvedAttempt?.targetBranch ||
8697
+ resolvedAttempt?.base_branch ||
8698
+ resolvedAttempt?.baseBranch ||
8699
+ null,
8700
+ )
8701
+ ) {
8557
8702
  console.log(
8558
8703
  `[monitor] smartPR(${shortId}): branch ${resolvedAttempt.branch} already in mergedBranchCache — skipping`,
8559
8704
  );
@@ -8582,7 +8727,7 @@ async function resolveAndTriggerSmartPR(shortId, status) {
8582
8727
  console.log(
8583
8728
  `[monitor] smartPR(${shortId}): branch ${resolvedAttempt.branch} confirmed merged — ${canFinalize ? "completing task and skipping PR flow" : "awaiting review gate"}`,
8584
8729
  );
8585
- mergedBranchCache.add(resolvedAttempt.branch);
8730
+ addMergedBranchCache(resolvedAttempt.branch);
8586
8731
  if (resolvedAttempt.task_id && canFinalize) {
8587
8732
  mergedTaskCache.add(resolvedAttempt.task_id);
8588
8733
  pendingMergeStrategyByTask.delete(
@@ -15099,5 +15244,3 @@ export {
15099
15244
  getContainerStatus,
15100
15245
  isContainerEnabled,
15101
15246
  };
15102
-
15103
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.34.4",
3
+ "version": "0.34.5",
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",
package/primary-agent.mjs CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  resetClaudeSession,
40
40
  initClaudeShell,
41
41
  } from "./claude-shell.mjs";
42
+ import { getModelsForExecutor } from "./task-complexity.mjs";
42
43
 
43
44
  /** Valid agent interaction modes */
44
45
  const VALID_MODES = ["ask", "agent", "plan"];
@@ -703,6 +704,7 @@ export function getAvailableAgents() {
703
704
  provider: adapter.provider,
704
705
  available: !disabled,
705
706
  busy,
707
+ models: getModelsForExecutor(adapter.provider), // use provider ("CODEX"/"COPILOT"/"CLAUDE") — always in the alias map
706
708
  capabilities: {
707
709
  sessions: typeof adapter.listSessions === "function",
708
710
  steering: typeof adapter.steer === "function",
@@ -141,6 +141,56 @@ const KANBAN_BACKENDS = [
141
141
  { value: "jira", label: "Atlassian Jira" },
142
142
  ];
143
143
 
144
+ function buildStableSetupDefaults({
145
+ projectName,
146
+ slug,
147
+ repoRoot,
148
+ bosunHome,
149
+ workspacesDir,
150
+ }) {
151
+ return {
152
+ projectName: projectName || slug?.split("/").pop() || "my-project",
153
+ repoSlug: slug,
154
+ repoRoot,
155
+ configDir: bosunHome,
156
+ bosunHome,
157
+ workspacesDir,
158
+ executors: [
159
+ {
160
+ name: "primary",
161
+ executor: "CODEX",
162
+ model: "gpt-5.3-codex",
163
+ weight: 100,
164
+ role: "primary",
165
+ authMode: "oauth",
166
+ apiKey: "",
167
+ baseUrl: "",
168
+ connections: [],
169
+ codexProfile: "",
170
+ },
171
+ ],
172
+ executor: "CODEX",
173
+ model: "gpt-5.3-codex",
174
+ kanbanBackend: "internal",
175
+ kanbanSyncPolicy: "internal-primary",
176
+ executorMode: "internal",
177
+ executorDistribution: "primary-only",
178
+ maxParallel: 4,
179
+ maxRetries: 3,
180
+ failoverStrategy: "next-in-line",
181
+ failoverCooldownMinutes: 5,
182
+ failoverDisableOnConsecutive: 3,
183
+ internalReplenishEnabled: false,
184
+ internalReplenishMin: 1,
185
+ internalReplenishMax: 2,
186
+ workflowAutomationEnabled: true,
187
+ copilotEnableAllMcpTools: false,
188
+ // Backward-compatible fields consumed by older setup UI revisions.
189
+ distribution: "primary-only",
190
+ cooldownMinutes: 5,
191
+ };
192
+ }
193
+
144
194
  const MIME_TYPES = {
145
195
  ".html": "text/html; charset=utf-8",
146
196
  ".css": "text/css; charset=utf-8",
@@ -443,22 +493,13 @@ function handleDefaults() {
443
493
 
444
494
  return {
445
495
  ok: true,
446
- defaults: {
447
- projectName: projectName || slug?.split("/").pop() || "my-project",
448
- repoSlug: slug,
496
+ defaults: buildStableSetupDefaults({
497
+ projectName,
498
+ slug,
449
499
  repoRoot,
450
- configDir: bosunHome,
451
500
  bosunHome,
452
501
  workspacesDir,
453
- executor: "COPILOT",
454
- model: "claude-sonnet-4",
455
- kanbanBackend: "internal",
456
- maxParallel: 6,
457
- maxRetries: 3,
458
- cooldownMinutes: 5,
459
- distribution: "weighted",
460
- failoverStrategy: "next-in-line",
461
- },
502
+ }),
462
503
  };
463
504
  }
464
505
 
@@ -676,7 +717,7 @@ function handleApply(body) {
676
717
  const envMap = {
677
718
  PROJECT_NAME: env.projectName || "",
678
719
  GITHUB_REPO: env.repoSlug || "",
679
- ORCHESTRATOR_ARGS: env.orchestratorArgs || `-MaxParallel ${env.maxParallel || 6}`,
720
+ ORCHESTRATOR_ARGS: env.orchestratorArgs || `-MaxParallel ${env.maxParallel || 4}`,
680
721
  EXECUTORS: env.executors || "",
681
722
  KANBAN_BACKEND: env.kanbanBackend || "internal",
682
723
  VK_PROJECT_DIR: bosunHome,
@@ -848,7 +889,7 @@ function handleApply(body) {
848
889
  cooldownMinutes: Number(env.failoverCooldownMinutes) || 5,
849
890
  disableOnConsecutiveFailures: Number(env.failoverDisableOnConsecutive) || 3,
850
891
  },
851
- distribution: configJson.distribution || env.executorDistribution || "weighted",
892
+ distribution: configJson.distribution || env.executorDistribution || "primary-only",
852
893
  };
853
894
 
854
895
  if (configJson.executorMode) config.executorMode = configJson.executorMode;