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.
- package/agent-endpoint.mjs +2 -2
- package/agent-event-bus.mjs +3 -3
- package/agent-work-report.mjs +1 -1
- package/maintenance.mjs +73 -6
- package/monitor.mjs +186 -43
- package/package.json +1 -1
- package/primary-agent.mjs +2 -0
- package/setup-web-server.mjs +56 -15
- package/setup.mjs +31 -22
- package/ui/app.js +2 -2
- package/ui/app.monolith.js +1 -0
- package/ui/components/agent-selector.js +181 -135
- package/ui/components/charts.js +10 -11
- package/ui/modules/agent-display.js +1 -1
- package/ui/modules/settings-schema.js +3 -0
- package/ui/modules/streaming.js +28 -14
- package/ui/setup.html +5 -6
- package/ui/styles/components.css +15 -0
- package/ui/styles/kanban.css +2 -2
- package/ui/styles/layout.css +12 -3
- package/ui/styles/sessions.css +24 -0
- package/ui/styles/variables.css +375 -0
- package/ui/tabs/chat.js +2 -2
- package/ui/tabs/control.js +1 -1
- package/ui/tabs/workflows.js +234 -20
- package/ui-server.mjs +306 -96
- package/workflow-templates/security.mjs +241 -241
package/agent-endpoint.mjs
CHANGED
|
@@ -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
|
+
|
package/agent-event-bus.mjs
CHANGED
|
@@ -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) {
|
package/agent-work-report.mjs
CHANGED
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
|
-
//
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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 || "
|
|
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 || "
|
|
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
|
|
4788
|
-
//
|
|
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
|
|
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
|
-
|
|
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 =
|
|
5240
|
-
|
|
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 &&
|
|
5657
|
+
(c) => c.branch && hasMergedBranchCache(c.branch),
|
|
5537
5658
|
);
|
|
5538
|
-
if (knownBranch) {
|
|
5539
|
-
const
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
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)
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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",
|
package/setup-web-server.mjs
CHANGED
|
@@ -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
|
|
448
|
-
|
|
496
|
+
defaults: buildStableSetupDefaults({
|
|
497
|
+
projectName,
|
|
498
|
+
slug,
|
|
449
499
|
repoRoot,
|
|
450
|
-
configDir: bosunHome,
|
|
451
500
|
bosunHome,
|
|
452
501
|
workspacesDir,
|
|
453
|
-
|
|
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 ||
|
|
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 || "
|
|
892
|
+
distribution: configJson.distribution || env.executorDistribution || "primary-only",
|
|
852
893
|
};
|
|
853
894
|
|
|
854
895
|
if (configJson.executorMode) config.executorMode = configJson.executorMode;
|