bosun 0.41.8 → 0.41.10
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/.env.example +1 -1
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +275 -40
- package/agent/agent-prompts.mjs +9 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/autofix.mjs +1 -1
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +47 -33
- package/config/context-shredding-config.mjs +1 -1
- package/config/repo-root.mjs +41 -33
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/git/sdk-conflict-resolver.mjs +1 -1
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +26 -8
- package/infra/session-tracker.mjs +30 -3
- package/package.json +12 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +288 -259
- package/server/ui-server.mjs +1323 -26
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-config.mjs +1 -1
- package/shell/codex-model-profiles.mjs +170 -30
- package/shell/codex-shell.mjs +63 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/telegram/telegram-sentinel.mjs +54 -3
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +15 -10
- package/ui/demo-defaults.js +334 -336
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/ui/tabs/workflows.js +766 -3
- package/workflow/workflow-engine.mjs +246 -5
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +1014 -184
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/_helpers.mjs +253 -0
- package/workflow-templates/agents.mjs +199 -226
- package/workflow-templates/github.mjs +106 -16
- package/workflow-templates/sub-workflows.mjs +233 -0
- package/workflow-templates/task-execution.mjs +125 -471
- package/workflow-templates/task-lifecycle.mjs +11 -48
- package/workspace/command-diagnostics.mjs +460 -0
- package/workspace/context-cache.mjs +396 -28
- package/workspace/worktree-manager.mjs +1 -1
package/shell/claude-shell.mjs
CHANGED
|
@@ -437,9 +437,22 @@ async function saveState() {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
function extractTaskHeading(msg) {
|
|
440
|
-
// Prompt first line is "# TASKID — Task Title"
|
|
440
|
+
// Prompt first line is "# Task: Task Title" (or legacy "# TASKID — Task Title").
|
|
441
441
|
// Return just the task title portion, or a short fallback.
|
|
442
442
|
const firstLine = msg.split(/\r?\n/)[0].replace(/^#+\s*/, '').trim();
|
|
443
|
+
if (!firstLine) return 'Execute Task';
|
|
444
|
+
const lowerLine = firstLine.toLowerCase();
|
|
445
|
+
if (lowerLine.startsWith('task')) {
|
|
446
|
+
let index = 4;
|
|
447
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
448
|
+
const separator = firstLine[index];
|
|
449
|
+
if (separator === ':' || separator === '-' || separator === '\u2014') {
|
|
450
|
+
index += 1;
|
|
451
|
+
while (index < firstLine.length && /\s/.test(firstLine[index])) index += 1;
|
|
452
|
+
const heading = firstLine.slice(index).trim();
|
|
453
|
+
if (heading) return heading;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
443
456
|
const dashIdx = firstLine.indexOf(' \u2014 ');
|
|
444
457
|
const heading = dashIdx !== -1 ? firstLine.slice(dashIdx + 3).trim() : firstLine;
|
|
445
458
|
return heading || 'Execute Task';
|
package/shell/codex-config.mjs
CHANGED
|
@@ -1345,7 +1345,7 @@ function ensureModelProviderSectionsFromEnv(toml, env = process.env) {
|
|
|
1345
1345
|
try {
|
|
1346
1346
|
const parsed = new URL(String(activeBaseUrl || ""));
|
|
1347
1347
|
const host = String(parsed.hostname || "").toLowerCase();
|
|
1348
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
|
|
1348
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
|
|
1349
1349
|
} catch {
|
|
1350
1350
|
return false;
|
|
1351
1351
|
}
|
|
@@ -9,6 +9,33 @@ function clean(value) {
|
|
|
9
9
|
return String(value ?? "").trim();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function isAzureOpenAIBaseUrl(value) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = value instanceof URL ? value : new URL(String(value || ""));
|
|
15
|
+
const host = String(parsed.hostname || "").toLowerCase();
|
|
16
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAzureOpenAIBaseUrl(value) {
|
|
23
|
+
const raw = clean(value);
|
|
24
|
+
if (!raw) return "";
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(raw);
|
|
27
|
+
if (!isAzureOpenAIBaseUrl(parsed)) {
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
parsed.pathname = "/openai/v1";
|
|
31
|
+
parsed.search = "";
|
|
32
|
+
parsed.hash = "";
|
|
33
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
34
|
+
} catch {
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
function normalizeProfileName(value, fallback = DEFAULT_ACTIVE_PROFILE) {
|
|
13
40
|
const raw = clean(value).toLowerCase();
|
|
14
41
|
if (!raw) return fallback;
|
|
@@ -19,20 +46,6 @@ function profilePrefix(name) {
|
|
|
19
46
|
return `CODEX_MODEL_PROFILE_${name.toUpperCase()}_`;
|
|
20
47
|
}
|
|
21
48
|
|
|
22
|
-
function inferGlobalProvider(env) {
|
|
23
|
-
const baseUrl = clean(env.OPENAI_BASE_URL).toLowerCase();
|
|
24
|
-
if ((() => {
|
|
25
|
-
try {
|
|
26
|
-
const parsed = new URL(baseUrl);
|
|
27
|
-
const host = String(parsed.hostname || "").toLowerCase();
|
|
28
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
})()) return "azure";
|
|
33
|
-
return "openai";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
49
|
function normalizeProvider(value, fallback) {
|
|
37
50
|
const raw = clean(value).toLowerCase();
|
|
38
51
|
if (!raw) return fallback;
|
|
@@ -48,6 +61,17 @@ function normalizeProvider(value, fallback) {
|
|
|
48
61
|
return fallback;
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
function hasEnvValue(env, key) {
|
|
65
|
+
return Boolean(key && clean(env?.[key]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inferProviderKindFromSection(name, section, fallback = "openai") {
|
|
69
|
+
if (isAzureOpenAIBaseUrl(section?.baseUrl)) return "azure";
|
|
70
|
+
const normalized = normalizeProvider(name, "");
|
|
71
|
+
if (normalized) return normalized;
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
function readProfileField(env, profileName, field) {
|
|
52
76
|
return clean(env[`${profilePrefix(profileName)}${field}`]);
|
|
53
77
|
}
|
|
@@ -57,8 +81,9 @@ function profileRecord(env, profileName, globalProvider) {
|
|
|
57
81
|
readProfileField(env, profileName, "PROVIDER"),
|
|
58
82
|
globalProvider,
|
|
59
83
|
);
|
|
84
|
+
const explicitModel = readProfileField(env, profileName, "MODEL");
|
|
60
85
|
const model =
|
|
61
|
-
|
|
86
|
+
explicitModel ||
|
|
62
87
|
(profileName === "xl" ? "gpt-5.3-codex" : profileName === "m" ? "gpt-5.1-codex-mini" : "");
|
|
63
88
|
const baseUrl = readProfileField(env, profileName, "BASE_URL");
|
|
64
89
|
const apiKey = readProfileField(env, profileName, "API_KEY");
|
|
@@ -68,6 +93,7 @@ function profileRecord(env, profileName, globalProvider) {
|
|
|
68
93
|
name: profileName,
|
|
69
94
|
provider,
|
|
70
95
|
model,
|
|
96
|
+
modelIsExplicit: Boolean(explicitModel),
|
|
71
97
|
baseUrl,
|
|
72
98
|
apiKey,
|
|
73
99
|
apiKeyEnv,
|
|
@@ -75,19 +101,101 @@ function profileRecord(env, profileName, globalProvider) {
|
|
|
75
101
|
};
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
function
|
|
104
|
+
function readCodexConfigRuntimeDefaults() {
|
|
79
105
|
try {
|
|
80
106
|
const configPath = resolve(homedir(), ".codex", "config.toml");
|
|
81
|
-
if (!existsSync(configPath))
|
|
107
|
+
if (!existsSync(configPath)) {
|
|
108
|
+
return { model: "", modelProvider: "", providers: {} };
|
|
109
|
+
}
|
|
82
110
|
const content = readFileSync(configPath, "utf8");
|
|
83
111
|
const head = content.split(/\n\[/)[0] || "";
|
|
84
|
-
const
|
|
85
|
-
|
|
112
|
+
const modelMatch = head.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
113
|
+
const modelProviderMatch = head.match(/^\s*model_provider\s*=\s*"([^"]+)"/m);
|
|
114
|
+
const providers = {};
|
|
115
|
+
const providerSectionRegex = /^\[model_providers\.([^\]]+)\]\s*([\s\S]*?)(?=^\[[^\]]+\]|$(?![\s\S]))/gm;
|
|
116
|
+
for (const match of content.matchAll(providerSectionRegex)) {
|
|
117
|
+
const [, rawName = "", body = ""] = match;
|
|
118
|
+
const name = clean(rawName);
|
|
119
|
+
if (!name) continue;
|
|
120
|
+
const baseUrlMatch = body.match(/^\s*base_url\s*=\s*"([^"]+)"/m);
|
|
121
|
+
const envKeyMatch = body.match(/^\s*env_key\s*=\s*"([^"]+)"/m);
|
|
122
|
+
providers[name] = {
|
|
123
|
+
name,
|
|
124
|
+
baseUrl: clean(baseUrlMatch?.[1]),
|
|
125
|
+
envKey: clean(envKeyMatch?.[1]),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
model: clean(modelMatch?.[1]),
|
|
130
|
+
modelProvider: clean(modelProviderMatch?.[1]),
|
|
131
|
+
providers,
|
|
132
|
+
};
|
|
86
133
|
} catch {
|
|
87
|
-
return "";
|
|
134
|
+
return { model: "", modelProvider: "", providers: {} };
|
|
88
135
|
}
|
|
89
136
|
}
|
|
90
137
|
|
|
138
|
+
function readCodexConfigTopLevelModel() {
|
|
139
|
+
return readCodexConfigRuntimeDefaults().model;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function selectConfigProviderForRuntime(configDefaults, env, preferredProvider = "") {
|
|
143
|
+
const providers = configDefaults?.providers || {};
|
|
144
|
+
const preferred = clean(preferredProvider).toLowerCase();
|
|
145
|
+
const entries = Object.values(providers).map((section) => ({
|
|
146
|
+
...section,
|
|
147
|
+
provider: inferProviderKindFromSection(section.name, section, preferred || "openai"),
|
|
148
|
+
}));
|
|
149
|
+
const matchingEntries = preferred
|
|
150
|
+
? entries.filter((section) => section.provider === preferred)
|
|
151
|
+
: entries;
|
|
152
|
+
const envBackedEntries = matchingEntries.filter(
|
|
153
|
+
(section) => !section.envKey || hasEnvValue(env, section.envKey),
|
|
154
|
+
);
|
|
155
|
+
const preferredNames = preferred === "azure"
|
|
156
|
+
? ["azure"]
|
|
157
|
+
: preferred === "openai"
|
|
158
|
+
? ["openai"]
|
|
159
|
+
: [];
|
|
160
|
+
const findNamed = (sections) => preferredNames
|
|
161
|
+
.map((name) => sections.find((section) => section.name === name))
|
|
162
|
+
.find(Boolean);
|
|
163
|
+
|
|
164
|
+
const configuredName = clean(configDefaults?.modelProvider);
|
|
165
|
+
if (configuredName) {
|
|
166
|
+
const configuredSection = providers[configuredName];
|
|
167
|
+
if (configuredSection) {
|
|
168
|
+
const configured = {
|
|
169
|
+
...configuredSection,
|
|
170
|
+
provider: inferProviderKindFromSection(
|
|
171
|
+
configuredSection.name,
|
|
172
|
+
configuredSection,
|
|
173
|
+
preferred || "openai",
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
const preferredMatches = !preferred || configured.provider === preferred;
|
|
177
|
+
if (preferredMatches && (!configured.envKey || hasEnvValue(env, configured.envKey))) {
|
|
178
|
+
return configured;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
findNamed(envBackedEntries) ||
|
|
185
|
+
envBackedEntries[0] ||
|
|
186
|
+
findNamed(matchingEntries) ||
|
|
187
|
+
matchingEntries[0] ||
|
|
188
|
+
null
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function inferGlobalProvider(env, configDefaults = null) {
|
|
193
|
+
const baseUrl = clean(env.OPENAI_BASE_URL).toLowerCase();
|
|
194
|
+
if (isAzureOpenAIBaseUrl(baseUrl)) return "azure";
|
|
195
|
+
const configured = selectConfigProviderForRuntime(configDefaults, env);
|
|
196
|
+
return configured?.provider || "openai";
|
|
197
|
+
}
|
|
198
|
+
|
|
91
199
|
/**
|
|
92
200
|
* Resolve codex model/provider profile configuration from env vars.
|
|
93
201
|
* Applies active profile values onto runtime env keys (`CODEX_MODEL`,
|
|
@@ -96,6 +204,7 @@ function readCodexConfigTopLevelModel() {
|
|
|
96
204
|
*/
|
|
97
205
|
export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
98
206
|
const sourceEnv = { ...envInput };
|
|
207
|
+
const configDefaults = readCodexConfigRuntimeDefaults();
|
|
99
208
|
const activeProfile = normalizeProfileName(
|
|
100
209
|
sourceEnv.CODEX_MODEL_PROFILE,
|
|
101
210
|
DEFAULT_ACTIVE_PROFILE,
|
|
@@ -105,7 +214,7 @@ export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
|
105
214
|
DEFAULT_SUBAGENT_PROFILE,
|
|
106
215
|
);
|
|
107
216
|
|
|
108
|
-
const globalProvider = inferGlobalProvider(sourceEnv);
|
|
217
|
+
const globalProvider = inferGlobalProvider(sourceEnv, configDefaults);
|
|
109
218
|
const active = profileRecord(sourceEnv, activeProfile, globalProvider);
|
|
110
219
|
const sub = profileRecord(sourceEnv, subagentProfile, globalProvider);
|
|
111
220
|
|
|
@@ -116,24 +225,51 @@ export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
|
116
225
|
if (active.model) {
|
|
117
226
|
env.CODEX_MODEL = active.model;
|
|
118
227
|
}
|
|
119
|
-
if (active.baseUrl) {
|
|
120
|
-
env.OPENAI_BASE_URL = active.baseUrl;
|
|
121
|
-
}
|
|
122
228
|
|
|
123
229
|
const profileApiKey = active.apiKey;
|
|
124
230
|
const resolvedProvider = active.provider || globalProvider;
|
|
231
|
+
const configProvider = selectConfigProviderForRuntime(
|
|
232
|
+
configDefaults,
|
|
233
|
+
sourceEnv,
|
|
234
|
+
resolvedProvider,
|
|
235
|
+
);
|
|
236
|
+
const runtimeBaseUrl =
|
|
237
|
+
clean(active.baseUrl) ||
|
|
238
|
+
clean(env.OPENAI_BASE_URL) ||
|
|
239
|
+
clean(configProvider?.baseUrl);
|
|
240
|
+
if (runtimeBaseUrl) {
|
|
241
|
+
const normalizedBaseUrl =
|
|
242
|
+
resolvedProvider === "azure"
|
|
243
|
+
? normalizeAzureOpenAIBaseUrl(runtimeBaseUrl)
|
|
244
|
+
: runtimeBaseUrl;
|
|
245
|
+
env.OPENAI_BASE_URL = normalizedBaseUrl;
|
|
246
|
+
active.baseUrl = normalizedBaseUrl;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!profileApiKey && configProvider?.envKey && hasEnvValue(sourceEnv, configProvider.envKey)) {
|
|
250
|
+
const configuredApiKey = clean(sourceEnv[configProvider.envKey]);
|
|
251
|
+
if (resolvedProvider === "azure") {
|
|
252
|
+
env.AZURE_OPENAI_API_KEY = configuredApiKey;
|
|
253
|
+
if (!env.OPENAI_API_KEY) {
|
|
254
|
+
env.OPENAI_API_KEY = configuredApiKey;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
env.OPENAI_API_KEY = configuredApiKey;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
125
260
|
|
|
126
261
|
// Azure deployments often differ from default model names.
|
|
127
262
|
// If the env is using Azure and the model is still the default,
|
|
128
263
|
// prefer the top-level ~/.codex/config.toml model when present.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
264
|
+
// The hardcoded profile fallback (e.g. "gpt-5.3-codex" for xl) should NOT
|
|
265
|
+
// block the config.toml override — only explicit env or profile fields should.
|
|
266
|
+
const runtimeModelExplicit = Boolean(clean(sourceEnv.CODEX_MODEL));
|
|
267
|
+
const shouldPreferAzureConfigModel =
|
|
133
268
|
resolvedProvider === "azure" &&
|
|
134
269
|
configModel &&
|
|
135
|
-
|
|
136
|
-
|
|
270
|
+
!active.modelIsExplicit &&
|
|
271
|
+
!runtimeModelExplicit;
|
|
272
|
+
if (shouldPreferAzureConfigModel) {
|
|
137
273
|
env.CODEX_MODEL = configModel;
|
|
138
274
|
active.model = configModel;
|
|
139
275
|
}
|
|
@@ -173,5 +309,9 @@ export function resolveCodexProfileRuntime(envInput = process.env) {
|
|
|
173
309
|
active,
|
|
174
310
|
subagent: sub,
|
|
175
311
|
provider: resolvedProvider,
|
|
312
|
+
/** The config.toml provider section selected for this runtime. */
|
|
313
|
+
configProvider: configProvider
|
|
314
|
+
? { name: configProvider.name, envKey: configProvider.envKey, baseUrl: configProvider.baseUrl }
|
|
315
|
+
: null,
|
|
176
316
|
};
|
|
177
317
|
}
|
package/shell/codex-shell.mjs
CHANGED
|
@@ -78,6 +78,63 @@ function normalizeTimeoutMs(value, { fallback = DEFAULT_TIMEOUT_MS, label = "tim
|
|
|
78
78
|
return normalized;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function isAzureOpenAIBaseUrl(value) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(String(value || ""));
|
|
84
|
+
const host = String(parsed.hostname || "").toLowerCase();
|
|
85
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCodexSdkRuntime(streamProviderOverrides, envInput = process.env) {
|
|
92
|
+
const resolved = resolveCodexProfileRuntime(envInput);
|
|
93
|
+
const { env: resolvedEnv, configProvider } = resolved;
|
|
94
|
+
const baseUrl = resolvedEnv.OPENAI_BASE_URL || "";
|
|
95
|
+
const isAzure = isAzureOpenAIBaseUrl(baseUrl);
|
|
96
|
+
const env = { ...resolvedEnv };
|
|
97
|
+
|
|
98
|
+
delete env.OPENAI_BASE_URL;
|
|
99
|
+
|
|
100
|
+
// Use the config.toml provider section name and env_key when available,
|
|
101
|
+
// so Bosun's config override is consistent with the user's config.toml
|
|
102
|
+
// instead of hardcoding "azure" / "AZURE_OPENAI_API_KEY".
|
|
103
|
+
const providerSectionName = (isAzure && configProvider?.name) || (isAzure ? "azure" : "openai");
|
|
104
|
+
const providerEnvKey = (isAzure && configProvider?.envKey) || (isAzure ? "AZURE_OPENAI_API_KEY" : "OPENAI_API_KEY");
|
|
105
|
+
|
|
106
|
+
if (isAzure && env.OPENAI_API_KEY && !env.AZURE_OPENAI_API_KEY) {
|
|
107
|
+
env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const providerName = isAzure ? "azure" : "openai";
|
|
111
|
+
const config = {
|
|
112
|
+
model_providers: {
|
|
113
|
+
[providerSectionName]: isAzure
|
|
114
|
+
? {
|
|
115
|
+
name: "Azure OpenAI",
|
|
116
|
+
base_url: baseUrl,
|
|
117
|
+
env_key: providerEnvKey,
|
|
118
|
+
wire_api: "responses",
|
|
119
|
+
...streamProviderOverrides,
|
|
120
|
+
}
|
|
121
|
+
: streamProviderOverrides,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (isAzure && env.CODEX_MODEL) {
|
|
126
|
+
config.model_provider = providerSectionName;
|
|
127
|
+
config.model = env.CODEX_MODEL;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
env,
|
|
132
|
+
config,
|
|
133
|
+
providerName,
|
|
134
|
+
streamIdleTimeoutMs: streamProviderOverrides.stream_idle_timeout_ms,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
81
138
|
function getInternalExecutorStreamConfig() {
|
|
82
139
|
try {
|
|
83
140
|
const cfg = loadConfig();
|
|
@@ -443,9 +500,6 @@ async function getThread() {
|
|
|
443
500
|
if (activeThread) return activeThread;
|
|
444
501
|
const threadOptions = buildThreadOptions();
|
|
445
502
|
|
|
446
|
-
const { env: resolvedEnv } = resolveCodexProfileRuntime(process.env);
|
|
447
|
-
Object.assign(process.env, resolvedEnv);
|
|
448
|
-
|
|
449
503
|
if (!codexInstance) {
|
|
450
504
|
const Cls = await loadCodexSdk();
|
|
451
505
|
if (!Cls) throw new Error("Codex SDK not available");
|
|
@@ -454,23 +508,16 @@ async function getThread() {
|
|
|
454
508
|
// even if config.toml hasn't been patched by codex-config.mjs yet.
|
|
455
509
|
// This is the most reliable path for Azure/Foundry deployments where
|
|
456
510
|
// dropped SSE streams ("response.failed") are the dominant failure mode.
|
|
457
|
-
const providerName = (() => {
|
|
458
|
-
try {
|
|
459
|
-
const parsed = new URL(String(resolvedEnv.OPENAI_BASE_URL || ""));
|
|
460
|
-
const host = String(parsed.hostname || "").toLowerCase();
|
|
461
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com")
|
|
462
|
-
? "azure"
|
|
463
|
-
: "openai";
|
|
464
|
-
} catch {
|
|
465
|
-
return "openai";
|
|
466
|
-
}
|
|
467
|
-
})();
|
|
468
511
|
const STREAM_IDLE_TIMEOUT_MS = 3_600_000; // 60 min — matches Azure max stream lifetime
|
|
469
512
|
const streamProviderOverrides = {
|
|
470
513
|
stream_idle_timeout_ms: STREAM_IDLE_TIMEOUT_MS,
|
|
471
514
|
stream_max_retries: 15,
|
|
472
515
|
request_max_retries: 6,
|
|
473
516
|
};
|
|
517
|
+
const runtime = buildCodexSdkRuntime(streamProviderOverrides, process.env);
|
|
518
|
+
|
|
519
|
+
Object.assign(process.env, runtime.env);
|
|
520
|
+
delete process.env.OPENAI_BASE_URL;
|
|
474
521
|
|
|
475
522
|
codexInstance = new Cls({
|
|
476
523
|
config: {
|
|
@@ -481,13 +528,11 @@ async function getThread() {
|
|
|
481
528
|
undo: true,
|
|
482
529
|
steer: true,
|
|
483
530
|
},
|
|
484
|
-
|
|
485
|
-
[providerName]: streamProviderOverrides,
|
|
486
|
-
},
|
|
531
|
+
...runtime.config,
|
|
487
532
|
},
|
|
488
533
|
});
|
|
489
534
|
|
|
490
|
-
console.log(`[codex-shell] created Codex instance (provider=${providerName}, stream_idle_timeout=${
|
|
535
|
+
console.log(`[codex-shell] created Codex instance (provider=${runtime.providerName}, stream_idle_timeout=${runtime.streamIdleTimeoutMs}ms, stream_max_retries=${streamProviderOverrides.stream_max_retries})`);
|
|
491
536
|
}
|
|
492
537
|
|
|
493
538
|
const transport = resolveCodexTransport();
|
|
@@ -91,22 +91,33 @@ async function discoverViaSDK(existingClient = null) {
|
|
|
91
91
|
// Try to connect to an already-running server
|
|
92
92
|
try {
|
|
93
93
|
const port = Number(process.env.OPENCODE_PORT || "4096");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
try {
|
|
95
|
+
client = sdk.createOpencodeClient({
|
|
96
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
97
|
+
timeout: 5_000,
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
client = sdk.createOpencodeClient({
|
|
101
|
+
hostname: "127.0.0.1",
|
|
102
|
+
port,
|
|
103
|
+
timeout: 5_000,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
100
106
|
} catch {
|
|
101
107
|
return null;
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
try {
|
|
112
|
+
const directory = process.cwd();
|
|
113
|
+
const requestOptions = directory
|
|
114
|
+
? { query: { directory } }
|
|
115
|
+
: undefined;
|
|
116
|
+
|
|
106
117
|
// Fetch provider list + auth methods in parallel
|
|
107
118
|
const [providerRes, authRes] = await Promise.all([
|
|
108
|
-
client.provider.list().catch(() => null),
|
|
109
|
-
client.provider.auth().catch(() => null),
|
|
119
|
+
client.provider.list(requestOptions).catch(() => null),
|
|
120
|
+
client.provider.auth(requestOptions).catch(() => null),
|
|
110
121
|
]);
|
|
111
122
|
|
|
112
123
|
if (!providerRes?.data) return null;
|
|
@@ -481,3 +492,4 @@ export function buildExecutorEntry(providerID, modelFullId, overrides = {}) {
|
|
|
481
492
|
export function invalidateCache() {
|
|
482
493
|
_providerCache = { data: null, ts: 0 };
|
|
483
494
|
}
|
|
495
|
+
|
package/task/task-executor.mjs
CHANGED
|
@@ -4278,6 +4278,34 @@ class TaskExecutor {
|
|
|
4278
4278
|
};
|
|
4279
4279
|
}
|
|
4280
4280
|
|
|
4281
|
+
resetTaskThrottleState(taskId, options = {}) {
|
|
4282
|
+
const key = normalizeTaskIdKey(taskId);
|
|
4283
|
+
if (!key) return false;
|
|
4284
|
+
|
|
4285
|
+
let changed = false;
|
|
4286
|
+
if (options.clearNoCommit !== false && this._noCommitCounts.delete(key)) {
|
|
4287
|
+
changed = true;
|
|
4288
|
+
}
|
|
4289
|
+
if (options.clearSkipUntil !== false && this._skipUntil.delete(key)) {
|
|
4290
|
+
changed = true;
|
|
4291
|
+
}
|
|
4292
|
+
if (options.clearCooldowns !== false && this._taskCooldowns.delete(key)) {
|
|
4293
|
+
changed = true;
|
|
4294
|
+
}
|
|
4295
|
+
if (this._idleContinueCounts.delete(key)) {
|
|
4296
|
+
changed = true;
|
|
4297
|
+
}
|
|
4298
|
+
if (this._repoAreaBlockedTasks.has(key)) {
|
|
4299
|
+
this._repoAreaBlockedTasks.delete(key);
|
|
4300
|
+
changed = true;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
if (changed) {
|
|
4304
|
+
this._saveNoCommitState();
|
|
4305
|
+
}
|
|
4306
|
+
return changed;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4281
4309
|
setBacklogReplenishmentConfig(patch = {}) {
|
|
4282
4310
|
if (!patch || typeof patch !== "object") {
|
|
4283
4311
|
return this.getBacklogReplenishmentConfig();
|
package/task/task-store.mjs
CHANGED
|
@@ -101,15 +101,25 @@ function resolveBosunHomeDir() {
|
|
|
101
101
|
return resolve(base, "bosun");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function resolveExplicitRepoRoot() {
|
|
105
|
+
const explicit = String(process.env.REPO_ROOT || "").trim();
|
|
106
|
+
if (!explicit) return null;
|
|
107
|
+
return resolve(explicit);
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function resolvePersistentStorePath() {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return resolve(
|
|
111
|
+
const explicitRepoRoot = resolveExplicitRepoRoot();
|
|
112
|
+
if (explicitRepoRoot) {
|
|
113
|
+
return resolve(explicitRepoRoot, ".bosun", ".cache", "kanban-state.json");
|
|
108
114
|
}
|
|
109
115
|
const bosunHome = resolveBosunHomeDir();
|
|
110
116
|
if (bosunHome) {
|
|
111
117
|
return resolve(bosunHome, ".cache", "kanban-state.json");
|
|
112
118
|
}
|
|
119
|
+
const repoRoot = inferRepoRoot(process.cwd());
|
|
120
|
+
if (repoRoot) {
|
|
121
|
+
return resolve(repoRoot, ".bosun", ".cache", "kanban-state.json");
|
|
122
|
+
}
|
|
113
123
|
return resolve(__dirname, "..", ".cache", "kanban-state.json");
|
|
114
124
|
}
|
|
115
125
|
|
|
@@ -2831,4 +2841,3 @@ export function getStaleInReviewTasks(maxAgeMs) {
|
|
|
2831
2841
|
(t) => t.status === "inreview" && t.lastActivityAt < cutoff,
|
|
2832
2842
|
);
|
|
2833
2843
|
}
|
|
2834
|
-
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
writeFileSync,
|
|
42
42
|
} from "node:fs";
|
|
43
43
|
import { readFile, writeFile, unlink } from "node:fs/promises";
|
|
44
|
-
import { resolve, dirname } from "node:path";
|
|
44
|
+
import { resolve, dirname, isAbsolute } from "node:path";
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { spawn } from "node:child_process";
|
|
47
47
|
import os from "node:os";
|
|
@@ -61,7 +61,55 @@ import {
|
|
|
61
61
|
|
|
62
62
|
const __filename = fileURLToPath(import.meta.url);
|
|
63
63
|
const __dirname = dirname(__filename);
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
// Resolve repo root from BOSUN_HOME config first, then fall back to git detection.
|
|
66
|
+
// This avoids depending on cwd being inside a git repo (daemon/sentinel may start
|
|
67
|
+
// from homedir or any arbitrary directory).
|
|
68
|
+
function _resolveSentinelRepoRoot() {
|
|
69
|
+
// 1. Explicit env
|
|
70
|
+
if (process.env.REPO_ROOT) {
|
|
71
|
+
const r = resolve(process.env.REPO_ROOT);
|
|
72
|
+
if (existsSync(r)) return r;
|
|
73
|
+
}
|
|
74
|
+
// 2. Check BOSUN_HOME workspace config for a primary repo
|
|
75
|
+
const bosunDir = resolveBosunConfigDir();
|
|
76
|
+
if (bosunDir) {
|
|
77
|
+
for (const cfgName of ["bosun.config.json", ".bosun.json", "bosun.json"]) {
|
|
78
|
+
const cfgPath = resolve(bosunDir, cfgName);
|
|
79
|
+
if (!existsSync(cfgPath)) continue;
|
|
80
|
+
try {
|
|
81
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
82
|
+
// Check workspace repos
|
|
83
|
+
const workspaces = Array.isArray(cfg.workspaces) ? cfg.workspaces : [];
|
|
84
|
+
for (const ws of workspaces) {
|
|
85
|
+
const repos = ws?.repos || ws?.repositories || [];
|
|
86
|
+
if (!Array.isArray(repos)) continue;
|
|
87
|
+
for (const repo of repos) {
|
|
88
|
+
const name = typeof repo === "string" ? repo.split("/").pop() : (repo?.name || repo?.id || "");
|
|
89
|
+
if (!name) continue;
|
|
90
|
+
const wsId = ws?.id || "default";
|
|
91
|
+
const repoPath = resolve(bosunDir, "workspaces", wsId, name);
|
|
92
|
+
if (existsSync(resolve(repoPath, ".git"))) return repoPath;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Check flat repositories
|
|
96
|
+
const repos = cfg.repositories || cfg.repos || [];
|
|
97
|
+
if (Array.isArray(repos)) {
|
|
98
|
+
for (const repo of repos) {
|
|
99
|
+
const repoPath = typeof repo === "string" ? repo : (repo?.path || repo?.repoRoot);
|
|
100
|
+
if (!repoPath) continue;
|
|
101
|
+
const resolved = resolve(isAbsolute(repoPath) ? repoPath : resolve(bosunDir, repoPath));
|
|
102
|
+
if (existsSync(resolve(resolved, ".git"))) return resolved;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch { /* invalid config */ }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 3. Fall back to standard repo-root resolution (git detection)
|
|
109
|
+
return resolveRepoRoot();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const repoRoot = _resolveSentinelRepoRoot();
|
|
65
113
|
const cacheDir = resolve(repoRoot, ".cache");
|
|
66
114
|
|
|
67
115
|
function resolveBosunConfigDir() {
|
|
@@ -1654,8 +1702,11 @@ async function startAndWaitForMonitor(reason) {
|
|
|
1654
1702
|
{
|
|
1655
1703
|
detached: true,
|
|
1656
1704
|
stdio: "ignore",
|
|
1705
|
+
windowsHide: process.platform === "win32",
|
|
1657
1706
|
env: spawnEnv,
|
|
1658
|
-
|
|
1707
|
+
// Use repoRoot if it has a .git dir; otherwise use homedir so spawn
|
|
1708
|
+
// never inherits a non-existent or non-repo cwd.
|
|
1709
|
+
cwd: existsSync(resolve(repoRoot, ".git")) ? repoRoot : os.homedir(),
|
|
1659
1710
|
},
|
|
1660
1711
|
);
|
|
1661
1712
|
|
package/tools/list-todos.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* list-todos — Scan codebase for TODO/FIXME/HACK/XXX/BUG comment markers
|
|
4
4
|
*
|
|
5
|
-
* Usage: node list-todos.mjs [rootDir] [--json]
|
|
5
|
+
* Usage: node list-todos.mjs [rootDir] [--json] [--help]
|
|
6
6
|
* rootDir defaults to cwd
|
|
7
7
|
* --json emit JSON array instead of human-readable output
|
|
8
|
+
* --help show usage and exit without scanning
|
|
8
9
|
*
|
|
9
10
|
* Exit 0 always (informational tool).
|
|
10
11
|
*/
|
|
@@ -12,6 +13,11 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
12
13
|
import { resolve, relative, extname } from "node:path";
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2);
|
|
16
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
|
|
17
|
+
console.log("Usage: node list-todos.mjs [rootDir] [--json] [--help]");
|
|
18
|
+
console.log("Scans for TODO/FIXME/HACK/XXX/TEMP/BUG comment markers.");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
15
21
|
const jsonMode = args.includes("--json");
|
|
16
22
|
const ROOT = args.find((a) => !a.startsWith("-")) || process.cwd();
|
|
17
23
|
|
package/ui/app.js
CHANGED
|
@@ -314,7 +314,7 @@ import { LogsTab } from "./tabs/logs.js";
|
|
|
314
314
|
import { TelemetryTab } from "./tabs/telemetry.js";
|
|
315
315
|
import { SettingsTab } from "./tabs/settings.js";
|
|
316
316
|
import { WorkflowsTab } from "./tabs/workflows.js";
|
|
317
|
-
import { LibraryTab } from "./tabs/library.js";
|
|
317
|
+
import { LibraryTab, LibraryMarketplaceTab } from "./tabs/library.js";
|
|
318
318
|
import { ManualFlowsTab } from "./tabs/manual-flows.js";
|
|
319
319
|
|
|
320
320
|
/* ── Shared components ── */
|
|
@@ -603,6 +603,7 @@ const TAB_COMPONENTS = {
|
|
|
603
603
|
workflows: WorkflowsTab,
|
|
604
604
|
"manual-flows": ManualFlowsTab,
|
|
605
605
|
library: LibraryTab,
|
|
606
|
+
marketplace: LibraryMarketplaceTab,
|
|
606
607
|
settings: SettingsTab,
|
|
607
608
|
};
|
|
608
609
|
|
|
@@ -1228,7 +1229,7 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
1228
1229
|
* Bottom Navigation
|
|
1229
1230
|
* ═══════════════════════════════════════════════ */
|
|
1230
1231
|
const PRIMARY_NAV_TABS = ["dashboard", "chat", "tasks", "agents"];
|
|
1231
|
-
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "workflows", "manual-flows", "settings"];
|
|
1232
|
+
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "marketplace", "workflows", "manual-flows", "settings"];
|
|
1232
1233
|
|
|
1233
1234
|
function getTabsById(ids) {
|
|
1234
1235
|
return ids
|