bosun 0.40.21 → 0.41.0
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/agent-custom-tools.mjs +23 -5
- package/agent/agent-pool.mjs +6 -2
- package/agent/primary-agent.mjs +81 -7
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/cli.mjs +208 -3
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +103 -3
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +8 -2
- package/infra/session-tracker.mjs +13 -3
- package/infra/test-runtime.mjs +267 -0
- package/package.json +8 -5
- package/server/setup-web-server.mjs +4 -1
- package/server/ui-server.mjs +1323 -20
- package/task/task-claims.mjs +6 -10
- package/ui/components/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +11746 -9470
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/voice-client-sdk.js +1 -1
- package/ui/modules/voice-client.js +33 -2
- package/ui/styles/components.css +514 -1
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/tasks.js +1052 -506
- package/ui/tabs/workflow-canvas-utils.mjs +30 -0
- package/ui/tabs/workflows.js +914 -298
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +24 -16
- package/workflow/project-detection.mjs +559 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +181 -30
- package/workflow/workflow-nodes.mjs +304 -6
- package/workflow/workflow-templates.mjs +92 -16
- package/workflow-templates/agents.mjs +20 -19
- package/workflow-templates/code-quality.mjs +20 -14
- package/workflow-templates/task-batch.mjs +3 -2
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +34 -8
- package/workspace/workspace-manager.mjs +151 -0
package/config/config.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
getModelsForExecutor,
|
|
32
32
|
MODEL_ALIASES,
|
|
33
33
|
} from "../task/task-complexity.mjs";
|
|
34
|
+
import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
|
|
34
35
|
|
|
35
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
37
|
|
|
@@ -133,7 +134,11 @@ function resolveConfigDir(repoRoot) {
|
|
|
133
134
|
const repoLocalConfigDir = resolveRepoLocalBosunDir(repoRoot);
|
|
134
135
|
if (repoLocalConfigDir) return repoLocalConfigDir;
|
|
135
136
|
|
|
136
|
-
// 3.
|
|
137
|
+
// 3. Tests must not fall through to the user's real global Bosun home.
|
|
138
|
+
const sandbox = ensureTestRuntimeSandbox();
|
|
139
|
+
if (sandbox?.configDir) return sandbox.configDir;
|
|
140
|
+
|
|
141
|
+
// 4. Platform-aware user home
|
|
137
142
|
const preferWindowsDirs =
|
|
138
143
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
139
144
|
const baseDir = preferWindowsDirs
|
|
@@ -1000,10 +1005,89 @@ class ExecutorScheduler {
|
|
|
1000
1005
|
this._roundRobinIndex = 0;
|
|
1001
1006
|
this._failureCounts = new Map(); // name → consecutive failures
|
|
1002
1007
|
this._disabledUntil = new Map(); // name → timestamp
|
|
1008
|
+
this._workspaceActiveCount = new Map(); // workspaceId → current active executor count
|
|
1009
|
+
this._workspaceConfigs = new Map(); // workspaceId → { maxConcurrent, pool, weight }
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Register workspace executor config for concurrency tracking.
|
|
1014
|
+
* @param {string} workspaceId
|
|
1015
|
+
* @param {{ maxConcurrent?: number, pool?: string, weight?: number }} wsExecutorConfig
|
|
1016
|
+
*/
|
|
1017
|
+
registerWorkspace(workspaceId, wsExecutorConfig = {}) {
|
|
1018
|
+
if (!workspaceId) return;
|
|
1019
|
+
this._workspaceConfigs.set(workspaceId, {
|
|
1020
|
+
maxConcurrent: wsExecutorConfig.maxConcurrent ?? 3,
|
|
1021
|
+
pool: wsExecutorConfig.pool ?? "shared",
|
|
1022
|
+
weight: wsExecutorConfig.weight ?? 1.0,
|
|
1023
|
+
executors: wsExecutorConfig.executors ?? null,
|
|
1024
|
+
});
|
|
1025
|
+
if (!this._workspaceActiveCount.has(workspaceId)) {
|
|
1026
|
+
this._workspaceActiveCount.set(workspaceId, 0);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Check if a workspace has available executor slots.
|
|
1032
|
+
* @param {string} [workspaceId]
|
|
1033
|
+
* @returns {boolean}
|
|
1034
|
+
*/
|
|
1035
|
+
hasAvailableSlot(workspaceId) {
|
|
1036
|
+
if (!workspaceId) return true; // no workspace scope — always available
|
|
1037
|
+
const config = this._workspaceConfigs.get(workspaceId);
|
|
1038
|
+
if (!config) return true; // no config registered — no limit
|
|
1039
|
+
const active = this._workspaceActiveCount.get(workspaceId) || 0;
|
|
1040
|
+
return active < config.maxConcurrent;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Acquire an executor slot for a workspace.
|
|
1045
|
+
* @param {string} [workspaceId]
|
|
1046
|
+
* @returns {boolean} true if slot acquired, false if at limit
|
|
1047
|
+
*/
|
|
1048
|
+
acquireSlot(workspaceId) {
|
|
1049
|
+
if (!workspaceId) return true;
|
|
1050
|
+
if (!this.hasAvailableSlot(workspaceId)) return false;
|
|
1051
|
+
this._workspaceActiveCount.set(
|
|
1052
|
+
workspaceId,
|
|
1053
|
+
(this._workspaceActiveCount.get(workspaceId) || 0) + 1,
|
|
1054
|
+
);
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Release an executor slot for a workspace.
|
|
1060
|
+
* @param {string} [workspaceId]
|
|
1061
|
+
*/
|
|
1062
|
+
releaseSlot(workspaceId) {
|
|
1063
|
+
if (!workspaceId) return;
|
|
1064
|
+
const current = this._workspaceActiveCount.get(workspaceId) || 0;
|
|
1065
|
+
this._workspaceActiveCount.set(workspaceId, Math.max(0, current - 1));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Get workspace executor usage summary.
|
|
1070
|
+
* @returns {Array<{ workspaceId: string, active: number, maxConcurrent: number, pool: string, weight: number }>}
|
|
1071
|
+
*/
|
|
1072
|
+
getWorkspaceSummary() {
|
|
1073
|
+
const result = [];
|
|
1074
|
+
for (const [wsId, config] of this._workspaceConfigs) {
|
|
1075
|
+
result.push({
|
|
1076
|
+
workspaceId: wsId,
|
|
1077
|
+
active: this._workspaceActiveCount.get(wsId) || 0,
|
|
1078
|
+
...config,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1003
1082
|
}
|
|
1004
1083
|
|
|
1005
1084
|
/** Get the next executor based on distribution strategy */
|
|
1006
|
-
next() {
|
|
1085
|
+
next(workspaceId) {
|
|
1086
|
+
// Check workspace slot availability before selecting
|
|
1087
|
+
if (workspaceId && !this.hasAvailableSlot(workspaceId)) {
|
|
1088
|
+
return null; // workspace at executor capacity
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1007
1091
|
const available = this._getAvailable();
|
|
1008
1092
|
if (!available.length) {
|
|
1009
1093
|
// All disabled — reset and use primary
|
|
@@ -1012,6 +1096,23 @@ class ExecutorScheduler {
|
|
|
1012
1096
|
return this.executors[0];
|
|
1013
1097
|
}
|
|
1014
1098
|
|
|
1099
|
+
// For dedicated pools, filter to workspace-assigned executors
|
|
1100
|
+
if (workspaceId) {
|
|
1101
|
+
const wsConfig = this._workspaceConfigs.get(workspaceId);
|
|
1102
|
+
if (wsConfig?.pool === "dedicated" && wsConfig.executors) {
|
|
1103
|
+
const dedicated = available.filter((e) =>
|
|
1104
|
+
wsConfig.executors.includes(e.name),
|
|
1105
|
+
);
|
|
1106
|
+
if (dedicated.length) {
|
|
1107
|
+
return this._selectByStrategy(dedicated);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return this._selectByStrategy(available);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
_selectByStrategy(available) {
|
|
1015
1116
|
switch (this.distribution) {
|
|
1016
1117
|
case "round-robin":
|
|
1017
1118
|
return this._roundRobin(available);
|
|
@@ -2419,4 +2520,3 @@ export {
|
|
|
2419
2520
|
resolveAgentRepoRoot,
|
|
2420
2521
|
};
|
|
2421
2522
|
export default loadConfig;
|
|
2422
|
-
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
|
|
22
22
|
const BOSUN_AUTH_STATE_PATH = join(homedir(), ".bosun", "github-auth-state.json");
|
|
23
23
|
|
|
24
|
+
/** Set of token types that have been invalidated due to 401 errors (resets after 5 min). */
|
|
25
|
+
const _invalidatedTypes = new Map(); // type → expiry timestamp
|
|
26
|
+
const INVALIDATION_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
|
|
24
28
|
// ── OAuth state loader ────────────────────────────────────────────────────────
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -30,13 +34,13 @@ const BOSUN_AUTH_STATE_PATH = join(homedir(), ".bosun", "github-auth-state.json"
|
|
|
30
34
|
async function loadSavedOAuthToken() {
|
|
31
35
|
// First check env override
|
|
32
36
|
const envToken = process.env.BOSUN_GITHUB_USER_TOKEN;
|
|
33
|
-
if (envToken) return envToken;
|
|
37
|
+
if (envToken && looksLikeRealToken(envToken)) return envToken;
|
|
34
38
|
|
|
35
39
|
try {
|
|
36
40
|
const raw = await readFile(BOSUN_AUTH_STATE_PATH, "utf8");
|
|
37
41
|
const data = JSON.parse(raw);
|
|
38
42
|
const token = data?.accessToken || data?.access_token || null;
|
|
39
|
-
if (!token) return null;
|
|
43
|
+
if (!token || !looksLikeRealToken(token)) return null;
|
|
40
44
|
|
|
41
45
|
// If there's an expiry, check it
|
|
42
46
|
if (data.expiresAt || data.expires_at) {
|
|
@@ -53,6 +57,23 @@ async function loadSavedOAuthToken() {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Sanity-check that a token string looks like a real GitHub token.
|
|
62
|
+
* Rejects placeholder values, very short strings, and obvious non-tokens.
|
|
63
|
+
*/
|
|
64
|
+
function looksLikeRealToken(token) {
|
|
65
|
+
if (!token || typeof token !== "string") return false;
|
|
66
|
+
const t = token.trim();
|
|
67
|
+
if (t.length < 8) return false;
|
|
68
|
+
// Reject common placeholder values
|
|
69
|
+
const PLACEHOLDERS = [
|
|
70
|
+
"oauth-token", "your-token", "token-here", "placeholder",
|
|
71
|
+
"xxxx", "test-token", "fake-token", "replace-me", "changeme",
|
|
72
|
+
];
|
|
73
|
+
if (PLACEHOLDERS.includes(t.toLowerCase())) return false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
// ── gh CLI fallback ───────────────────────────────────────────────────────────
|
|
57
78
|
|
|
58
79
|
/**
|
|
@@ -97,27 +118,41 @@ async function verifyToken(token) {
|
|
|
97
118
|
|
|
98
119
|
/**
|
|
99
120
|
* Get the best available token for GitHub API calls.
|
|
121
|
+
* Skips token sources that have been recently invalidated (401 errors).
|
|
100
122
|
*
|
|
101
123
|
* @param {Object} [options]
|
|
102
124
|
* @param {string} [options.owner] - repo owner (for installation token resolution)
|
|
103
125
|
* @param {string} [options.repo] - repo name (for installation token resolution)
|
|
104
126
|
* @param {boolean} [options.verify] - verify token via /user API (default: false for perf)
|
|
127
|
+
* @param {string} [options.skipType] - skip a specific token type (used during retry)
|
|
105
128
|
* @returns {Promise<{token: string, type: 'oauth'|'installation'|'gh-cli'|'env', login?: string}>}
|
|
106
129
|
*/
|
|
107
130
|
export async function getGitHubToken(options = {}) {
|
|
108
|
-
const { owner, repo, verify = false } = options;
|
|
131
|
+
const { owner, repo, verify = false, skipType } = options;
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
|
|
134
|
+
// Purge expired invalidations
|
|
135
|
+
for (const [key, expiry] of _invalidatedTypes) {
|
|
136
|
+
if (now >= expiry) _invalidatedTypes.delete(key);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isSkipped = (type) =>
|
|
140
|
+
type === skipType || _invalidatedTypes.has(type);
|
|
109
141
|
|
|
110
142
|
// ── 1. OAuth user token ───────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
if (!isSkipped("oauth")) {
|
|
144
|
+
const oauthToken = await loadSavedOAuthToken();
|
|
145
|
+
if (oauthToken) {
|
|
146
|
+
const login = verify ? await verifyToken(oauthToken) : undefined;
|
|
147
|
+
if (!verify || login) {
|
|
148
|
+
return { token: oauthToken, type: "oauth", login: login ?? undefined };
|
|
149
|
+
}
|
|
150
|
+
// verify failed — skip oauth for this resolution cycle
|
|
116
151
|
}
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
// ── 2. GitHub App installation token ─────────────────────────────────────
|
|
120
|
-
if (owner && repo && isAppConfigured()) {
|
|
155
|
+
if (!isSkipped("installation") && owner && repo && isAppConfigured()) {
|
|
121
156
|
try {
|
|
122
157
|
const { token } = await getInstallationTokenForRepo(owner, repo);
|
|
123
158
|
if (token) {
|
|
@@ -129,19 +164,23 @@ export async function getGitHubToken(options = {}) {
|
|
|
129
164
|
}
|
|
130
165
|
|
|
131
166
|
// ── 3. gh CLI token ───────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
167
|
+
if (!isSkipped("gh-cli")) {
|
|
168
|
+
const ghCliToken = await getGhCliToken();
|
|
169
|
+
if (ghCliToken) {
|
|
170
|
+
return { token: ghCliToken, type: "gh-cli" };
|
|
171
|
+
}
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
// ── 4. Environment variable fallback ─────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
if (!isSkipped("env")) {
|
|
176
|
+
const envToken =
|
|
177
|
+
process.env.GITHUB_TOKEN ||
|
|
178
|
+
process.env.GH_TOKEN ||
|
|
179
|
+
process.env.GITHUB_PAT ||
|
|
180
|
+
"";
|
|
181
|
+
if (envToken) {
|
|
182
|
+
return { token: envToken, type: "env" };
|
|
183
|
+
}
|
|
145
184
|
}
|
|
146
185
|
|
|
147
186
|
throw new Error(
|
|
@@ -263,3 +302,15 @@ export async function getAuthStatus() {
|
|
|
263
302
|
message: `No GitHub authentication available. ${hints.join(". ")}`,
|
|
264
303
|
};
|
|
265
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Mark a token type as invalid for 5 minutes so getGitHubToken() will skip it.
|
|
308
|
+
* Call this when a GitHub API call returns 401 to trigger fallback to the next source.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} type - 'oauth' | 'installation' | 'gh-cli' | 'env'
|
|
311
|
+
*/
|
|
312
|
+
export function invalidateTokenType(type) {
|
|
313
|
+
if (type) {
|
|
314
|
+
_invalidatedTypes.set(type, Date.now() + INVALIDATION_TTL_MS);
|
|
315
|
+
}
|
|
316
|
+
}
|