bosun 0.28.1 → 0.28.3
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 +7 -0
- package/README.md +12 -4
- package/desktop/launch.mjs +31 -2
- package/desktop/main.mjs +51 -6
- package/desktop/package.json +8 -0
- package/desktop-shortcut.mjs +20 -1
- package/monitor.mjs +43 -19
- package/package.json +3 -2
- package/preflight.mjs +3 -1
- package/pwsh-runtime.mjs +62 -0
- package/rotate-agent-logs.sh +8 -7
- package/setup.mjs +226 -8
- package/task-executor.mjs +13 -3
- package/telegram-bot.mjs +163 -8
- package/ui/app.js +2 -0
- package/ui/demo.html +8 -0
- package/ui/modules/icons.js +14 -0
- package/ui/modules/router.js +1 -0
- package/ui/modules/settings-schema.js +9 -0
- package/ui/modules/state.js +59 -0
- package/ui/styles.css +88 -0
- package/ui/tabs/control.js +48 -3
- package/ui/tabs/telemetry.js +167 -0
- package/ui-server.mjs +128 -0
- package/workspace-manager.mjs +57 -11
package/.env.example
CHANGED
|
@@ -169,6 +169,13 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
169
169
|
# Priority threshold for immediate delivery: 1=critical only, 2=critical+errors (default: 1)
|
|
170
170
|
# TELEGRAM_IMMEDIATE_PRIORITY=1
|
|
171
171
|
|
|
172
|
+
# ─── Auto-Delete Old Messages ──────────────────────────────────────────────────────────
|
|
173
|
+
# Automatically delete bot messages older than N days to keep chat tidy.
|
|
174
|
+
# Set to 0 to disable. Default: 3 days.
|
|
175
|
+
# Note: Telegram’s API may silently skip messages older than 48 h in private
|
|
176
|
+
# chats — those will just remain; no error is raised.
|
|
177
|
+
# TELEGRAM_HISTORY_RETENTION_DAYS=3
|
|
178
|
+
|
|
172
179
|
# ─── Presence & Multi-Instance Coordination ──────────────────────────────────
|
|
173
180
|
# Presence heartbeat allows discovering multiple bosun instances.
|
|
174
181
|
# Heartbeat interval in seconds (default: 60)
|
package/README.md
CHANGED
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
Bosun is a production-grade supervisor for AI coding agents. It routes tasks across executors, automates PR lifecycles, and keeps operators in control through Telegram, the Mini App dashboard, and optional WhatsApp notifications.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://bosun.virtengine.com">Website</a> · <a href="https://bosun.virtengine.com/docs/">Docs</a> · <a href="https://github.com/virtengine/bosun?tab=readme-ov-file#bosun">GitHub</a> · <a href="https://www.npmjs.com/package/bosun">npm</a> · <a href="https://github.com/virtengine/bosun/issues">Issues</a>
|
|
10
|
+
</p>
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="site/social-card.png" alt="bosun — AI agent supervisor" width="100%" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://github.com/virtengine/bosun/actions/workflows/ci.yaml"><img src="https://github.com/virtengine/bosun/actions/workflows/ci.yaml/badge.svg?branch=main" alt="CI" /></a>
|
|
18
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License" /></a>
|
|
19
|
+
<a href="https://www.npmjs.com/package/bosun"><img src="https://img.shields.io/npm/v/bosun.svg" alt="npm" /></a>
|
|
20
|
+
</p>
|
|
13
21
|
|
|
14
22
|
---
|
|
15
23
|
|
package/desktop/launch.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync, spawn } from "node:child_process";
|
|
@@ -7,6 +7,28 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const desktopDir = resolve(__dirname);
|
|
8
8
|
const binName = process.platform === "win32" ? "electron.cmd" : "electron";
|
|
9
9
|
const electronBin = resolve(desktopDir, "node_modules", ".bin", binName);
|
|
10
|
+
const chromeSandbox = resolve(
|
|
11
|
+
desktopDir,
|
|
12
|
+
"node_modules",
|
|
13
|
+
"electron",
|
|
14
|
+
"dist",
|
|
15
|
+
"chrome-sandbox",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
function shouldDisableSandbox() {
|
|
19
|
+
if (process.env.BOSUN_DESKTOP_DISABLE_SANDBOX === "1") return true;
|
|
20
|
+
if (process.platform !== "linux") return false;
|
|
21
|
+
if (!existsSync(chromeSandbox)) return true;
|
|
22
|
+
try {
|
|
23
|
+
const stats = statSync(chromeSandbox);
|
|
24
|
+
const mode = stats.mode & 0o7777;
|
|
25
|
+
const isRootOwned = stats.uid === 0;
|
|
26
|
+
const isSetuid = mode === 0o4755;
|
|
27
|
+
return !(isRootOwned && isSetuid);
|
|
28
|
+
} catch {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
|
|
11
33
|
function ensureElectronInstalled() {
|
|
12
34
|
if (existsSync(electronBin)) return true;
|
|
@@ -28,11 +50,18 @@ function launch() {
|
|
|
28
50
|
process.exit(1);
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
const
|
|
53
|
+
const disableSandbox = shouldDisableSandbox();
|
|
54
|
+
const args = [desktopDir];
|
|
55
|
+
if (disableSandbox) {
|
|
56
|
+
args.push("--no-sandbox", "--disable-gpu-sandbox");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const child = spawn(electronBin, args, {
|
|
32
60
|
stdio: "inherit",
|
|
33
61
|
env: {
|
|
34
62
|
...process.env,
|
|
35
63
|
BOSUN_DESKTOP: "1",
|
|
64
|
+
...(disableSandbox ? { ELECTRON_DISABLE_SANDBOX: "1" } : {}),
|
|
36
65
|
},
|
|
37
66
|
});
|
|
38
67
|
|
package/desktop/main.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, BrowserWindow } from "electron";
|
|
1
|
+
import { app, BrowserWindow, session } from "electron";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -18,6 +18,19 @@ let runtimeConfigLoaded = false;
|
|
|
18
18
|
|
|
19
19
|
const DAEMON_PID_FILE = resolve(homedir(), ".cache", "bosun", "daemon.pid");
|
|
20
20
|
|
|
21
|
+
// Local/private-network patterns — TLS cert bypass for the embedded UI server
|
|
22
|
+
const LOCAL_HOSTNAME_RE = [
|
|
23
|
+
/^127\./,
|
|
24
|
+
/^192\.168\./,
|
|
25
|
+
/^10\./,
|
|
26
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
27
|
+
/^::1$/,
|
|
28
|
+
/^localhost$/i,
|
|
29
|
+
];
|
|
30
|
+
function isLocalHost(hostname) {
|
|
31
|
+
return LOCAL_HOSTNAME_RE.some((re) => re.test(hostname));
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
function parseBoolEnv(value, fallback) {
|
|
22
35
|
if (value === undefined || value === null) return fallback;
|
|
23
36
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -163,7 +176,7 @@ async function resolveDaemonUiUrl() {
|
|
|
163
176
|
async function ensureDaemonRunning() {
|
|
164
177
|
const autoStart = parseBoolEnv(
|
|
165
178
|
process.env.BOSUN_DESKTOP_AUTO_START_DAEMON,
|
|
166
|
-
|
|
179
|
+
false,
|
|
167
180
|
);
|
|
168
181
|
if (!autoStart) return;
|
|
169
182
|
|
|
@@ -239,6 +252,7 @@ async function buildUiUrl() {
|
|
|
239
252
|
|
|
240
253
|
async function createMainWindow() {
|
|
241
254
|
if (mainWindow) return;
|
|
255
|
+
const iconPath = resolveBosunRuntimePath("logo.png");
|
|
242
256
|
|
|
243
257
|
mainWindow = new BrowserWindow({
|
|
244
258
|
width: 1280,
|
|
@@ -246,6 +260,7 @@ async function createMainWindow() {
|
|
|
246
260
|
minWidth: 960,
|
|
247
261
|
minHeight: 640,
|
|
248
262
|
backgroundColor: "#0b0b0c",
|
|
263
|
+
...(existsSync(iconPath) ? { icon: iconPath } : {}),
|
|
249
264
|
show: false,
|
|
250
265
|
webPreferences: {
|
|
251
266
|
contextIsolation: true,
|
|
@@ -269,9 +284,34 @@ async function createMainWindow() {
|
|
|
269
284
|
|
|
270
285
|
async function bootstrap() {
|
|
271
286
|
try {
|
|
287
|
+
if (process.env.ELECTRON_DISABLE_SANDBOX === "1") {
|
|
288
|
+
app.commandLine.appendSwitch("no-sandbox");
|
|
289
|
+
app.commandLine.appendSwitch("disable-gpu-sandbox");
|
|
290
|
+
}
|
|
272
291
|
app.setAppUserModelId("com.virtengine.bosun");
|
|
292
|
+
const iconPath = resolveBosunRuntimePath("logo.png");
|
|
293
|
+
if (existsSync(iconPath)) {
|
|
294
|
+
try {
|
|
295
|
+
app.setIcon(iconPath);
|
|
296
|
+
} catch {
|
|
297
|
+
/* best effort */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
273
300
|
process.chdir(resolveBosunRoot());
|
|
274
301
|
await loadRuntimeConfig();
|
|
302
|
+
|
|
303
|
+
// Bypass TLS verification for the local embedded UI server.
|
|
304
|
+
// setCertificateVerifyProc works at the OpenSSL level — it fires before
|
|
305
|
+
// the higher-level `certificate-error` event and stops the repeated
|
|
306
|
+
// "handshake failed" logs from Chromium's ssl_client_socket_impl.
|
|
307
|
+
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
308
|
+
if (isLocalHost(request.hostname)) {
|
|
309
|
+
callback(0); // 0 = verified OK
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
callback(-3); // -3 = use Chromium default chain verification
|
|
313
|
+
});
|
|
314
|
+
|
|
275
315
|
await ensureDaemonRunning();
|
|
276
316
|
await createMainWindow();
|
|
277
317
|
await maybeAutoUpdate();
|
|
@@ -331,10 +371,15 @@ app.on("before-quit", () => {
|
|
|
331
371
|
app.on(
|
|
332
372
|
"certificate-error",
|
|
333
373
|
(event, _webContents, url, _error, _certificate, callback) => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
374
|
+
try {
|
|
375
|
+
const hostname = new URL(url).hostname;
|
|
376
|
+
if ((uiOrigin && url.startsWith(uiOrigin)) || isLocalHost(hostname)) {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
callback(true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
// malformed URL — fall through
|
|
338
383
|
}
|
|
339
384
|
callback(false);
|
|
340
385
|
},
|
package/desktop/package.json
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": "VirtEngine Maintainers <hello@virtengine.com>",
|
|
7
|
+
"homepage": "https://bosun.virtengine.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/virtengine/bosun.git",
|
|
11
|
+
"directory": "/desktop"
|
|
12
|
+
},
|
|
13
|
+
|
|
6
14
|
"main": "main.mjs",
|
|
7
15
|
"description": "Electron wrapper for the Bosun desktop portal",
|
|
8
16
|
"scripts": {
|
package/desktop-shortcut.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Linux: .desktop entry
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { spawnSync, execSync } from "node:child_process";
|
|
13
13
|
import {
|
|
14
14
|
existsSync,
|
|
15
15
|
readFileSync,
|
|
@@ -182,22 +182,41 @@ function installMacShortcut(desktopDir) {
|
|
|
182
182
|
|
|
183
183
|
function installLinuxShortcut(desktopDir) {
|
|
184
184
|
const desktopPath = resolve(desktopDir, `${APP_NAME}.desktop`);
|
|
185
|
+
const appDir = resolve(homedir(), ".local", "share", "applications");
|
|
186
|
+
const appPath = resolve(appDir, `${APP_NAME}.desktop`);
|
|
187
|
+
const iconPath = resolve(__dirname, "logo.png");
|
|
185
188
|
const content = [
|
|
186
189
|
"[Desktop Entry]",
|
|
187
190
|
"Type=Application",
|
|
188
191
|
`Name=${APP_NAME}`,
|
|
189
192
|
"Comment=Bosun Desktop Portal",
|
|
193
|
+
`Icon=${iconPath}`,
|
|
190
194
|
`Exec=${buildShellCommand()}`,
|
|
191
195
|
`Path=${getWorkingDirectory()}`,
|
|
192
196
|
"Terminal=false",
|
|
193
197
|
"StartupNotify=true",
|
|
194
198
|
"Categories=Development;Utility;",
|
|
199
|
+
"NoDisplay=false",
|
|
195
200
|
"",
|
|
196
201
|
].join("\n");
|
|
197
202
|
|
|
198
203
|
try {
|
|
199
204
|
writeFileSync(desktopPath, content, "utf8");
|
|
200
205
|
chmodSync(desktopPath, 0o755);
|
|
206
|
+
mkdirSync(appDir, { recursive: true });
|
|
207
|
+
writeFileSync(appPath, content, "utf8");
|
|
208
|
+
chmodSync(appPath, 0o755);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
execSync(`gio set "${desktopPath}" metadata::trusted true`, {
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
});
|
|
214
|
+
execSync(`gio set "${appPath}" metadata::trusted true`, {
|
|
215
|
+
stdio: "ignore",
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
/* best effort */
|
|
219
|
+
}
|
|
201
220
|
return {
|
|
202
221
|
success: true,
|
|
203
222
|
method: "Linux desktop entry",
|
package/monitor.mjs
CHANGED
|
@@ -157,6 +157,7 @@ import { WorkspaceMonitor } from "./workspace-monitor.mjs";
|
|
|
157
157
|
import { VkLogStream } from "./vk-log-stream.mjs";
|
|
158
158
|
import { VKErrorResolver } from "./vk-error-resolver.mjs";
|
|
159
159
|
import { createAnomalyDetector } from "./anomaly-detector.mjs";
|
|
160
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
160
161
|
import {
|
|
161
162
|
getWorktreeManager,
|
|
162
163
|
acquireWorktree,
|
|
@@ -7555,12 +7556,21 @@ function buildPlannerTaskDescription({
|
|
|
7555
7556
|
reason,
|
|
7556
7557
|
numTasks,
|
|
7557
7558
|
runtimeContext,
|
|
7559
|
+
userPrompt,
|
|
7558
7560
|
}) {
|
|
7559
7561
|
return [
|
|
7560
7562
|
"## Task Planner — Auto-created by bosun",
|
|
7561
7563
|
"",
|
|
7562
7564
|
`**Trigger reason:** ${reason || "manual"}`,
|
|
7563
7565
|
`**Requested task count:** ${numTasks}`,
|
|
7566
|
+
...(userPrompt
|
|
7567
|
+
? [
|
|
7568
|
+
"",
|
|
7569
|
+
"### User Planning Prompt",
|
|
7570
|
+
"",
|
|
7571
|
+
userPrompt,
|
|
7572
|
+
]
|
|
7573
|
+
: []),
|
|
7564
7574
|
"",
|
|
7565
7575
|
"### Planner Prompt (Injected by bosun)",
|
|
7566
7576
|
"",
|
|
@@ -8689,6 +8699,7 @@ async function triggerTaskPlanner(
|
|
|
8689
8699
|
details,
|
|
8690
8700
|
{
|
|
8691
8701
|
taskCount,
|
|
8702
|
+
userPrompt,
|
|
8692
8703
|
notify = true,
|
|
8693
8704
|
preferredMode,
|
|
8694
8705
|
allowCodexWhenDisabled = false,
|
|
@@ -8724,6 +8735,7 @@ async function triggerTaskPlanner(
|
|
|
8724
8735
|
try {
|
|
8725
8736
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8726
8737
|
taskCount,
|
|
8738
|
+
userPrompt,
|
|
8727
8739
|
notify,
|
|
8728
8740
|
});
|
|
8729
8741
|
} catch (kanbanErr) {
|
|
@@ -8753,6 +8765,7 @@ async function triggerTaskPlanner(
|
|
|
8753
8765
|
}
|
|
8754
8766
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8755
8767
|
taskCount,
|
|
8768
|
+
userPrompt,
|
|
8756
8769
|
notify,
|
|
8757
8770
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8758
8771
|
});
|
|
@@ -8761,6 +8774,7 @@ async function triggerTaskPlanner(
|
|
|
8761
8774
|
try {
|
|
8762
8775
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8763
8776
|
taskCount,
|
|
8777
|
+
userPrompt,
|
|
8764
8778
|
notify,
|
|
8765
8779
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8766
8780
|
});
|
|
@@ -8784,6 +8798,7 @@ async function triggerTaskPlanner(
|
|
|
8784
8798
|
|
|
8785
8799
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8786
8800
|
taskCount,
|
|
8801
|
+
userPrompt,
|
|
8787
8802
|
notify,
|
|
8788
8803
|
});
|
|
8789
8804
|
}
|
|
@@ -8818,7 +8833,7 @@ async function triggerTaskPlanner(
|
|
|
8818
8833
|
async function triggerTaskPlannerViaKanban(
|
|
8819
8834
|
reason,
|
|
8820
8835
|
details,
|
|
8821
|
-
{ taskCount, notify = true } = {},
|
|
8836
|
+
{ taskCount, userPrompt, notify = true } = {},
|
|
8822
8837
|
) {
|
|
8823
8838
|
const defaultPlannerTaskCount = Number(
|
|
8824
8839
|
process.env.TASK_PLANNER_DEFAULT_COUNT || "30",
|
|
@@ -8844,12 +8859,15 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8844
8859
|
);
|
|
8845
8860
|
}
|
|
8846
8861
|
|
|
8847
|
-
const desiredTitle =
|
|
8862
|
+
const desiredTitle = userPrompt
|
|
8863
|
+
? `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"}) — ${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? "…" : ""}`
|
|
8864
|
+
: `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"})`;
|
|
8848
8865
|
const desiredDescription = buildPlannerTaskDescription({
|
|
8849
8866
|
plannerPrompt,
|
|
8850
8867
|
reason,
|
|
8851
8868
|
numTasks,
|
|
8852
8869
|
runtimeContext,
|
|
8870
|
+
userPrompt,
|
|
8853
8871
|
});
|
|
8854
8872
|
|
|
8855
8873
|
// Check for existing planner tasks to avoid duplicates
|
|
@@ -8941,7 +8959,7 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8941
8959
|
async function triggerTaskPlannerViaCodex(
|
|
8942
8960
|
reason,
|
|
8943
8961
|
details,
|
|
8944
|
-
{ taskCount, notify = true, allowWhenDisabled = false } = {},
|
|
8962
|
+
{ taskCount, userPrompt, notify = true, allowWhenDisabled = false } = {},
|
|
8945
8963
|
) {
|
|
8946
8964
|
if (!codexEnabled && !allowWhenDisabled) {
|
|
8947
8965
|
throw new Error(
|
|
@@ -8973,6 +8991,17 @@ async function triggerTaskPlannerViaCodex(
|
|
|
8973
8991
|
"## Execution Context",
|
|
8974
8992
|
`- Trigger reason: ${reason || "manual"}`,
|
|
8975
8993
|
`- Requested task count: ${numTasks}`,
|
|
8994
|
+
...(userPrompt
|
|
8995
|
+
? [
|
|
8996
|
+
"",
|
|
8997
|
+
"## User Planning Prompt",
|
|
8998
|
+
"",
|
|
8999
|
+
userPrompt,
|
|
9000
|
+
"",
|
|
9001
|
+
"Incorporate the above user prompt into any relevant planning decisions.",
|
|
9002
|
+
]
|
|
9003
|
+
: []),
|
|
9004
|
+
"",
|
|
8976
9005
|
"Context JSON:",
|
|
8977
9006
|
"```json",
|
|
8978
9007
|
safeJsonBlock(runtimeContext),
|
|
@@ -10590,21 +10619,16 @@ async function startProcess() {
|
|
|
10590
10619
|
let orchestratorArgs = [...scriptArgs];
|
|
10591
10620
|
|
|
10592
10621
|
if (scriptLower.endsWith(".ps1")) {
|
|
10593
|
-
const
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
const pwshLabel = configuredPwsh
|
|
10604
|
-
? `PWSH_PATH (${configuredPwsh})`
|
|
10605
|
-
: bundledPwshExists
|
|
10606
|
-
? `bundled pwsh (${bundledPwsh})`
|
|
10607
|
-
: "pwsh on PATH";
|
|
10622
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
10623
|
+
if (!pwshRuntime.exists) {
|
|
10624
|
+
const pwshLabel =
|
|
10625
|
+
pwshRuntime.source === "env"
|
|
10626
|
+
? `PWSH_PATH (${pwshRuntime.command})`
|
|
10627
|
+
: pwshRuntime.source === "bundled"
|
|
10628
|
+
? `bundled pwsh (${pwshRuntime.command})`
|
|
10629
|
+
: pwshRuntime.source === "powershell"
|
|
10630
|
+
? `powershell on PATH`
|
|
10631
|
+
: "pwsh on PATH";
|
|
10608
10632
|
const pauseMs = Math.max(orchestratorPauseMs, 60_000);
|
|
10609
10633
|
const pauseMin = Math.max(1, Math.round(pauseMs / 60_000));
|
|
10610
10634
|
monitorSafeModeUntil = Math.max(monitorSafeModeUntil, Date.now() + pauseMs);
|
|
@@ -10622,7 +10646,7 @@ async function startProcess() {
|
|
|
10622
10646
|
setTimeout(startProcess, pauseMs);
|
|
10623
10647
|
return;
|
|
10624
10648
|
}
|
|
10625
|
-
orchestratorCmd =
|
|
10649
|
+
orchestratorCmd = pwshRuntime.command;
|
|
10626
10650
|
orchestratorArgs = ["-File", scriptPath, ...scriptArgs];
|
|
10627
10651
|
} else if (scriptLower.endsWith(".sh")) {
|
|
10628
10652
|
const shellCmd =
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
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",
|
|
7
|
-
"author": "VirtEngine Maintainers <
|
|
7
|
+
"author": "VirtEngine Maintainers <hello@virtengine.com>",
|
|
8
8
|
"homepage": "https://bosun.virtengine.com",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"sdk-conflict-resolver.mjs",
|
|
153
153
|
"session-tracker.mjs",
|
|
154
154
|
"setup.mjs",
|
|
155
|
+
"pwsh-runtime.mjs",
|
|
155
156
|
"shared-knowledge.mjs",
|
|
156
157
|
"shared-state-manager.mjs",
|
|
157
158
|
"shared-workspace-cli.mjs",
|
package/preflight.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
4
5
|
|
|
5
6
|
const isWindows = process.platform === "win32";
|
|
6
7
|
const MIN_FREE_GB = Number(process.env.BOSUN_MIN_FREE_GB || "10");
|
|
@@ -179,6 +180,7 @@ function checkToolchain() {
|
|
|
179
180
|
"node",
|
|
180
181
|
shellMode ? "shell" : "pwsh",
|
|
181
182
|
]);
|
|
183
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
182
184
|
|
|
183
185
|
const tools = [
|
|
184
186
|
checkToolVersion(
|
|
@@ -213,7 +215,7 @@ function checkToolchain() {
|
|
|
213
215
|
),
|
|
214
216
|
checkToolVersion(
|
|
215
217
|
"pwsh",
|
|
216
|
-
|
|
218
|
+
pwshRuntime.command,
|
|
217
219
|
["-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"],
|
|
218
220
|
"Install PowerShell 7+ (pwsh) and ensure it is on PATH.",
|
|
219
221
|
),
|
package/pwsh-runtime.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const BUNDLED_PWSH_PATH = resolve(__dirname, ".cache", "bosun", "pwsh", "pwsh");
|
|
8
|
+
|
|
9
|
+
function commandExists(cmd) {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`${process.platform === "win32" ? "where" : "which"} ${cmd}`, {
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
});
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPathLike(value) {
|
|
21
|
+
return value.includes("/") || value.includes("\\");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePwshRuntime({ preferBundled = true } = {}) {
|
|
25
|
+
const configured = String(process.env.PWSH_PATH || "").trim();
|
|
26
|
+
if (configured) {
|
|
27
|
+
if (isPathLike(configured)) {
|
|
28
|
+
if (existsSync(configured)) {
|
|
29
|
+
return { command: configured, source: "env", exists: true };
|
|
30
|
+
}
|
|
31
|
+
return { command: configured, source: "env", exists: false };
|
|
32
|
+
}
|
|
33
|
+
if (commandExists(configured)) {
|
|
34
|
+
return { command: configured, source: "env", exists: true };
|
|
35
|
+
}
|
|
36
|
+
return { command: configured, source: "env", exists: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (preferBundled && existsSync(BUNDLED_PWSH_PATH)) {
|
|
40
|
+
return { command: BUNDLED_PWSH_PATH, source: "bundled", exists: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (commandExists("pwsh")) {
|
|
44
|
+
return { command: "pwsh", source: "path", exists: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.platform === "win32" && commandExists("powershell")) {
|
|
48
|
+
return { command: "powershell", source: "powershell", exists: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { command: "pwsh", source: "missing", exists: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePwshCommand(options = {}) {
|
|
55
|
+
return resolvePwshRuntime(options).command;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasPwshRuntime(options = {}) {
|
|
59
|
+
return resolvePwshRuntime(options).exists;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { BUNDLED_PWSH_PATH };
|
package/rotate-agent-logs.sh
CHANGED
|
@@ -15,11 +15,12 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
15
15
|
LOG_DIR="$REPO_ROOT/.cache/agent-work-logs"
|
|
16
16
|
ARCHIVE_DIR="$LOG_DIR/archive"
|
|
17
17
|
|
|
18
|
-
# Retention periods
|
|
19
|
-
STREAM_RETENTION_DAYS=30
|
|
20
|
-
ERROR_RETENTION_DAYS=90
|
|
21
|
-
SESSION_RETENTION_COUNT=100
|
|
22
|
-
ARCHIVE_RETENTION_DAYS=180
|
|
18
|
+
# Retention periods (override via env)
|
|
19
|
+
STREAM_RETENTION_DAYS="${AGENT_WORK_STREAM_RETENTION_DAYS:-30}"
|
|
20
|
+
ERROR_RETENTION_DAYS="${AGENT_WORK_ERROR_RETENTION_DAYS:-90}"
|
|
21
|
+
SESSION_RETENTION_COUNT="${AGENT_WORK_SESSION_RETENTION_COUNT:-100}"
|
|
22
|
+
ARCHIVE_RETENTION_DAYS="${AGENT_WORK_ARCHIVE_RETENTION_DAYS:-180}"
|
|
23
|
+
METRICS_ROTATION_ENABLED="${AGENT_WORK_METRICS_ROTATION_ENABLED:-true}"
|
|
23
24
|
|
|
24
25
|
# ── Functions ───────────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -111,10 +112,10 @@ if [ -f "$LOG_DIR/agent-alerts.jsonl" ]; then
|
|
|
111
112
|
rotate_file "$LOG_DIR/agent-alerts.jsonl" "$ALERTS_ARCHIVE" "$STREAM_RETENTION_DAYS"
|
|
112
113
|
fi
|
|
113
114
|
|
|
114
|
-
# Metrics log is kept indefinitely (compressed monthly)
|
|
115
|
+
# Metrics log is kept indefinitely (compressed monthly unless disabled)
|
|
115
116
|
if [ -f "$LOG_DIR/agent-metrics.jsonl" ]; then
|
|
116
117
|
# Only rotate on first day of month
|
|
117
|
-
if [ "$(date +%d)" = "01" ]; then
|
|
118
|
+
if [ "$METRICS_ROTATION_ENABLED" != "false" ] && [ "$(date +%d)" = "01" ]; then
|
|
118
119
|
METRICS_ARCHIVE="agent-metrics-$(date -d 'last month' +%Y%m).jsonl.gz"
|
|
119
120
|
rotate_file "$LOG_DIR/agent-metrics.jsonl" "$METRICS_ARCHIVE" ""
|
|
120
121
|
fi
|