bosun 0.41.7 → 0.41.9
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/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +251 -11
- package/agent/agent-prompts.mjs +5 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +4 -1
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +25 -7
- package/infra/session-tracker.mjs +30 -3
- package/package.json +10 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +287 -258
- package/server/ui-server.mjs +218 -23
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-model-profiles.mjs +166 -29
- package/shell/codex-shell.mjs +56 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- 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 +2 -0
- package/ui/demo-defaults.js +6 -6
- 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/workflow/workflow-engine.mjs +16 -4
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +489 -153
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/github.mjs +106 -16
- package/workspace/worktree-manager.mjs +1 -1
package/desktop/preload.mjs
CHANGED
|
@@ -94,4 +94,23 @@ contextBridge.exposeInMainWorld("veDesktop", {
|
|
|
94
94
|
setScope: (id, isGlobal) =>
|
|
95
95
|
ipcRenderer.invoke("bosun:shortcuts:setScope", { id, isGlobal }),
|
|
96
96
|
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remote connection settings API.
|
|
100
|
+
* Available in the renderer via `window.veDesktop.connection.*`
|
|
101
|
+
*/
|
|
102
|
+
connection: {
|
|
103
|
+
/** Get the current remote connection config + active status. */
|
|
104
|
+
get: () => ipcRenderer.invoke("bosun:connection:get"),
|
|
105
|
+
|
|
106
|
+
/** Save remote connection config. { enabled, endpoint, apiKey } */
|
|
107
|
+
set: (config) => ipcRenderer.invoke("bosun:connection:set", config),
|
|
108
|
+
|
|
109
|
+
/** Test connectivity to a remote endpoint. { endpoint, apiKey } */
|
|
110
|
+
test: (endpoint, apiKey) =>
|
|
111
|
+
ipcRenderer.invoke("bosun:connection:test", { endpoint, apiKey }),
|
|
112
|
+
|
|
113
|
+
/** Open the interactive remote connection setup dialog. */
|
|
114
|
+
setup: () => ipcRenderer.invoke("bosun:connection:setup"),
|
|
115
|
+
},
|
|
97
116
|
});
|
package/entrypoint.mjs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* entrypoint.mjs — Unified Bosun entrypoint for Docker, Desktop, and CLI.
|
|
5
|
+
*
|
|
6
|
+
* This is the single process owner for all Bosun child processes.
|
|
7
|
+
* It starts the unified UI server (which includes setup wizard if needed),
|
|
8
|
+
* spawns the monitor loop as a managed child, handles SIGTERM/SIGINT
|
|
9
|
+
* gracefully, and provides a /healthz endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Environment detection:
|
|
12
|
+
* BOSUN_DOCKER=1 → container mode (bind 0.0.0.0, logs to stdout, tini as PID 1)
|
|
13
|
+
* BOSUN_DESKTOP=1 → Electron mode (bind 127.0.0.1, auto-open browser)
|
|
14
|
+
* (neither) → bare CLI mode
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node entrypoint.mjs # default start
|
|
18
|
+
* BOSUN_DOCKER=1 node entrypoint.mjs # Docker container
|
|
19
|
+
* BOSUN_DESKTOP=1 node entrypoint.mjs # Desktop app
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { resolve, dirname } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import {
|
|
27
|
+
getHealthStatus,
|
|
28
|
+
markSetupComplete as _markSetupComplete,
|
|
29
|
+
setComponentStatus,
|
|
30
|
+
setShuttingDown,
|
|
31
|
+
setMonitorCircuitBroken,
|
|
32
|
+
setMode,
|
|
33
|
+
} from "./infra/health-status.mjs";
|
|
34
|
+
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = dirname(__filename);
|
|
37
|
+
|
|
38
|
+
const TAG = "[entrypoint]";
|
|
39
|
+
const isDocker = process.env.BOSUN_DOCKER === "1";
|
|
40
|
+
const isDesktop = process.env.BOSUN_DESKTOP === "1";
|
|
41
|
+
|
|
42
|
+
// Synchronise mode flags with the shared health module
|
|
43
|
+
setMode({ docker: isDocker, desktop: isDesktop });
|
|
44
|
+
|
|
45
|
+
// ── Data directory resolution ───────────────────────────────────────────────
|
|
46
|
+
// In Docker: /data (volume mount)
|
|
47
|
+
// Otherwise: respect BOSUN_HOME / BOSUN_DIR, or let config.mjs resolve
|
|
48
|
+
function resolveDataDir() {
|
|
49
|
+
if (isDocker) {
|
|
50
|
+
const dataDir = process.env.BOSUN_HOME || "/data";
|
|
51
|
+
process.env.BOSUN_HOME = dataDir;
|
|
52
|
+
process.env.BOSUN_DIR = dataDir;
|
|
53
|
+
return dataDir;
|
|
54
|
+
}
|
|
55
|
+
return process.env.BOSUN_HOME || process.env.BOSUN_DIR || "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Child process management ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** @type {Map<string, import("node:child_process").ChildProcess>} */
|
|
61
|
+
const children = new Map();
|
|
62
|
+
|
|
63
|
+
/** @type {boolean} */
|
|
64
|
+
let shuttingDown = false;
|
|
65
|
+
|
|
66
|
+
/** @type {boolean} */
|
|
67
|
+
let setupComplete = false;
|
|
68
|
+
|
|
69
|
+
// Track monitor crash history for circuit breaker
|
|
70
|
+
const monitorCrashTimestamps = [];
|
|
71
|
+
const MONITOR_CIRCUIT_BREAKER_WINDOW_MS = 60_000;
|
|
72
|
+
const MONITOR_CIRCUIT_BREAKER_MAX_CRASHES = 3;
|
|
73
|
+
let monitorCircuitBroken = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Spawn a managed child process.
|
|
77
|
+
* @param {string} name - Human label for logs
|
|
78
|
+
* @param {string} script - Path to the script to run
|
|
79
|
+
* @param {string[]} args - Arguments
|
|
80
|
+
* @param {object} [opts] - spawn options overrides
|
|
81
|
+
* @returns {import("node:child_process").ChildProcess}
|
|
82
|
+
*/
|
|
83
|
+
function spawnChild(name, script, args = [], opts = {}) {
|
|
84
|
+
if (shuttingDown) return null;
|
|
85
|
+
|
|
86
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
87
|
+
cwd: opts.cwd || __dirname,
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
env: { ...process.env, ...opts.env },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
children.set(name, child);
|
|
93
|
+
setComponentStatus(name, "running");
|
|
94
|
+
|
|
95
|
+
child.stdout?.on("data", (chunk) => {
|
|
96
|
+
process.stdout.write(chunk);
|
|
97
|
+
});
|
|
98
|
+
child.stderr?.on("data", (chunk) => {
|
|
99
|
+
process.stderr.write(chunk);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
child.on("exit", (code, signal) => {
|
|
103
|
+
children.delete(name);
|
|
104
|
+
setComponentStatus(name, "stopped");
|
|
105
|
+
|
|
106
|
+
if (shuttingDown) {
|
|
107
|
+
console.log(`${TAG} ${name} exited (shutdown)`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.warn(`${TAG} ${name} exited (code=${code} signal=${signal})`);
|
|
112
|
+
|
|
113
|
+
// Self-restart exit code (75) means monitor wants a clean re-fork
|
|
114
|
+
if (name === "monitor" && code === 75) {
|
|
115
|
+
console.log(`${TAG} monitor requested restart (exit code 75)`);
|
|
116
|
+
startMonitor();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Circuit breaker for monitor crashes
|
|
121
|
+
if (name === "monitor") {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
monitorCrashTimestamps.push(now);
|
|
124
|
+
// Trim old entries outside window
|
|
125
|
+
while (
|
|
126
|
+
monitorCrashTimestamps.length > 0 &&
|
|
127
|
+
now - monitorCrashTimestamps[0] > MONITOR_CIRCUIT_BREAKER_WINDOW_MS
|
|
128
|
+
) {
|
|
129
|
+
monitorCrashTimestamps.shift();
|
|
130
|
+
}
|
|
131
|
+
if (monitorCrashTimestamps.length >= MONITOR_CIRCUIT_BREAKER_MAX_CRASHES) {
|
|
132
|
+
monitorCircuitBroken = true;
|
|
133
|
+
setMonitorCircuitBroken(true);
|
|
134
|
+
console.error(
|
|
135
|
+
`${TAG} monitor circuit breaker tripped: ${monitorCrashTimestamps.length} crashes in ${MONITOR_CIRCUIT_BREAKER_WINDOW_MS / 1000}s — stopping restarts`,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Restart with backoff
|
|
141
|
+
const delay = Math.min(5000 * monitorCrashTimestamps.length, 30_000);
|
|
142
|
+
console.log(`${TAG} restarting monitor in ${delay}ms`);
|
|
143
|
+
setTimeout(() => startMonitor(), delay);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on("error", (err) => {
|
|
148
|
+
console.error(`${TAG} ${name} spawn error: ${err.message}`);
|
|
149
|
+
children.delete(name);
|
|
150
|
+
setComponentStatus(name, "error");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return child;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Monitor lifecycle ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function startMonitor() {
|
|
159
|
+
if (shuttingDown || monitorCircuitBroken) return;
|
|
160
|
+
if (children.has("monitor")) return;
|
|
161
|
+
|
|
162
|
+
const monitorScript = resolve(__dirname, "infra", "monitor.mjs");
|
|
163
|
+
if (!existsSync(monitorScript)) {
|
|
164
|
+
console.error(`${TAG} monitor script not found: ${monitorScript}`);
|
|
165
|
+
setComponentStatus("monitor", "error");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const monitorArgs = [];
|
|
170
|
+
// Pass through relevant CLI args
|
|
171
|
+
if (process.env.BOSUN_DIR) {
|
|
172
|
+
monitorArgs.push("--config-dir", process.env.BOSUN_DIR);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`${TAG} starting monitor`);
|
|
176
|
+
spawnChild("monitor", monitorScript, monitorArgs);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Graceful shutdown ───────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
const SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
182
|
+
|
|
183
|
+
async function shutdown(signal) {
|
|
184
|
+
if (shuttingDown) return;
|
|
185
|
+
shuttingDown = true;
|
|
186
|
+
setShuttingDown(true);
|
|
187
|
+
console.log(`${TAG} ${signal} received — shutting down`);
|
|
188
|
+
|
|
189
|
+
// Send SIGTERM to all children
|
|
190
|
+
for (const [name, child] of children.entries()) {
|
|
191
|
+
console.log(`${TAG} sending SIGTERM to ${name} (pid=${child.pid})`);
|
|
192
|
+
try {
|
|
193
|
+
child.kill("SIGTERM");
|
|
194
|
+
} catch {
|
|
195
|
+
/* best effort */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Wait for children to exit, with a hard timeout
|
|
200
|
+
const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;
|
|
201
|
+
while (children.size > 0 && Date.now() < deadline) {
|
|
202
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// SIGKILL any stragglers
|
|
206
|
+
for (const [name, child] of children.entries()) {
|
|
207
|
+
console.warn(`${TAG} force-killing ${name} (pid=${child.pid})`);
|
|
208
|
+
try {
|
|
209
|
+
child.kill("SIGKILL");
|
|
210
|
+
} catch {
|
|
211
|
+
/* best effort */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
219
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
220
|
+
|
|
221
|
+
// Windows: ensure cleanup on exit
|
|
222
|
+
process.on("exit", () => {
|
|
223
|
+
for (const [, child] of children.entries()) {
|
|
224
|
+
try {
|
|
225
|
+
child.kill("SIGTERM");
|
|
226
|
+
} catch {
|
|
227
|
+
/* best effort */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── Health endpoint ─────────────────────────────────────────────────────────
|
|
233
|
+
// Re-export from the side-effect-free health module so existing callers
|
|
234
|
+
// that import from entrypoint.mjs keep working.
|
|
235
|
+
export { getHealthStatus } from "./infra/health-status.mjs";
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Mark setup as complete (called from ui-server when setup finishes).
|
|
239
|
+
*/
|
|
240
|
+
export function markSetupComplete() {
|
|
241
|
+
const wasSetupComplete = setupComplete;
|
|
242
|
+
setupComplete = true;
|
|
243
|
+
_markSetupComplete();
|
|
244
|
+
|
|
245
|
+
// If we just transitioned from "not complete" to "complete", and the process
|
|
246
|
+
// is still running, ensure the monitor is started when not already running.
|
|
247
|
+
if (!wasSetupComplete && !shuttingDown && !children.has("monitor")) {
|
|
248
|
+
try {
|
|
249
|
+
startMonitor();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// Log but do not crash if automatic monitor start fails
|
|
252
|
+
console.error(
|
|
253
|
+
`${TAG} failed to start monitor after setup completion: ${
|
|
254
|
+
err && err.message ? err.message : err
|
|
255
|
+
}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Main startup ────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
async function main() {
|
|
264
|
+
const dataDir = resolveDataDir();
|
|
265
|
+
|
|
266
|
+
console.log(`${TAG} starting bosun`);
|
|
267
|
+
console.log(`${TAG} mode: ${isDocker ? "docker" : isDesktop ? "desktop" : "cli"}`);
|
|
268
|
+
if (dataDir) {
|
|
269
|
+
console.log(`${TAG} data dir: ${dataDir}`);
|
|
270
|
+
// Ensure data directory exists
|
|
271
|
+
mkdirSync(dataDir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Check if setup is needed ──────────────────────────────────────────
|
|
275
|
+
try {
|
|
276
|
+
const { shouldRunSetup } = await import("./setup.mjs");
|
|
277
|
+
setupComplete = !shouldRunSetup();
|
|
278
|
+
if (setupComplete) _markSetupComplete();
|
|
279
|
+
} catch {
|
|
280
|
+
// If setup.mjs fails to load, assume setup is needed
|
|
281
|
+
setupComplete = false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Start the unified UI server ───────────────────────────────────────
|
|
285
|
+
// The ui-server now handles both /setup (wizard) and / (portal)
|
|
286
|
+
try {
|
|
287
|
+
const { startTelegramUiServer } = await import("./server/ui-server.mjs");
|
|
288
|
+
|
|
289
|
+
const host = isDocker ? "0.0.0.0" : isDesktop ? "127.0.0.1" : undefined;
|
|
290
|
+
const port = Number(process.env.BOSUN_PORT || process.env.PORT || process.env.TELEGRAM_UI_PORT || "") || 3080;
|
|
291
|
+
|
|
292
|
+
const server = await startTelegramUiServer({
|
|
293
|
+
host,
|
|
294
|
+
port,
|
|
295
|
+
// Pass setup state so ui-server knows whether to redirect to /setup
|
|
296
|
+
setupMode: !setupComplete,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (server) {
|
|
300
|
+
setComponentStatus("server", "running");
|
|
301
|
+
console.log(`${TAG} ui server started`);
|
|
302
|
+
} else {
|
|
303
|
+
console.warn(`${TAG} ui server returned null — may be a duplicate instance`);
|
|
304
|
+
setComponentStatus("server", "error");
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(`${TAG} failed to start ui server: ${err.message}`);
|
|
308
|
+
setComponentStatus("server", "error");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Start monitor (only if setup is complete) ─────────────────────────
|
|
312
|
+
if (setupComplete) {
|
|
313
|
+
startMonitor();
|
|
314
|
+
} else {
|
|
315
|
+
console.log(`${TAG} setup not complete — monitor will start after setup finishes`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// In Docker mode, log to stdout that we're ready
|
|
319
|
+
if (isDocker) {
|
|
320
|
+
console.log(`${TAG} container ready`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Entry ───────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
if (process.argv[1] === __filename) {
|
|
327
|
+
main().catch((err) => {
|
|
328
|
+
console.error(`${TAG} fatal: ${err.message}`);
|
|
329
|
+
console.error(err.stack);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-status.mjs — Side-effect-free module for Bosun health reporting.
|
|
3
|
+
*
|
|
4
|
+
* This module is safe to import from any context (ui-server, entrypoint, tests)
|
|
5
|
+
* without triggering process spawning or other startup side effects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TAG = "[health-status]";
|
|
9
|
+
|
|
10
|
+
/** @type {{ monitor: string, server: string }} */
|
|
11
|
+
const componentStatus = {
|
|
12
|
+
monitor: "stopped",
|
|
13
|
+
server: "stopped",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let shuttingDown = false;
|
|
17
|
+
let setupComplete = false;
|
|
18
|
+
let monitorCircuitBroken = false;
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
let isDocker = process.env.BOSUN_DOCKER === "1";
|
|
21
|
+
let isDesktop = process.env.BOSUN_DESKTOP === "1";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns current health status object.
|
|
25
|
+
* @returns {object}
|
|
26
|
+
*/
|
|
27
|
+
export function getHealthStatus() {
|
|
28
|
+
return {
|
|
29
|
+
status: shuttingDown ? "shutting_down" : monitorCircuitBroken ? "degraded" : "ok",
|
|
30
|
+
setup: setupComplete,
|
|
31
|
+
monitor: componentStatus.monitor,
|
|
32
|
+
server: componentStatus.server,
|
|
33
|
+
uptime: Math.floor((Date.now() - startedAt) / 1000),
|
|
34
|
+
docker: isDocker,
|
|
35
|
+
desktop: isDesktop,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update a named component's status.
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @param {string} status
|
|
43
|
+
*/
|
|
44
|
+
export function setComponentStatus(name, status) {
|
|
45
|
+
componentStatus[name] = status;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Mark setup as complete. */
|
|
49
|
+
export function markSetupComplete() {
|
|
50
|
+
setupComplete = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @returns {boolean} */
|
|
54
|
+
export function isSetupComplete() {
|
|
55
|
+
return setupComplete;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Mark the process as shutting down. */
|
|
59
|
+
export function setShuttingDown(value = true) {
|
|
60
|
+
shuttingDown = value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mark the monitor circuit breaker as tripped. */
|
|
64
|
+
export function setMonitorCircuitBroken(value = true) {
|
|
65
|
+
monitorCircuitBroken = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Allow callers (entrypoint) to override mode flags after env detection. */
|
|
69
|
+
export function setMode({ docker, desktop } = {}) {
|
|
70
|
+
if (docker !== undefined) isDocker = Boolean(docker);
|
|
71
|
+
if (desktop !== undefined) isDesktop = Boolean(desktop);
|
|
72
|
+
}
|
|
@@ -200,6 +200,58 @@ const FRAMEWORK_DOMAIN_MAP = Object.freeze({
|
|
|
200
200
|
|
|
201
201
|
/** Resource types managed by the library */
|
|
202
202
|
export const RESOURCE_TYPES = Object.freeze(["prompt", "agent", "skill", "mcp", "custom-tool"]);
|
|
203
|
+
export const AGENT_LIBRARY_CATEGORIES = Object.freeze(["task", "interactive", "voice"]);
|
|
204
|
+
export const INTERACTIVE_AGENT_MODES = Object.freeze(["ask", "agent", "plan", "web", "instant", "custom", "voice"]);
|
|
205
|
+
|
|
206
|
+
export function normalizeAgentProfileType(rawType, options = {}) {
|
|
207
|
+
const value = String(rawType || "").trim().toLowerCase();
|
|
208
|
+
if (value === "voice" || value === "task" || value === "chat") return value;
|
|
209
|
+
if (options?.voiceAgent === true) return "voice";
|
|
210
|
+
return "task";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function normalizeAgentLibraryCategory(rawCategory, options = {}) {
|
|
214
|
+
const value = String(rawCategory || "").trim().toLowerCase();
|
|
215
|
+
if (AGENT_LIBRARY_CATEGORIES.includes(value)) return value;
|
|
216
|
+
const profileType = normalizeAgentProfileType(options?.agentType, { voiceAgent: options?.voiceAgent });
|
|
217
|
+
if (profileType === "voice") return "voice";
|
|
218
|
+
if (profileType === "chat") return "interactive";
|
|
219
|
+
return "task";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizeInteractiveAgentMode(rawMode, options = {}) {
|
|
223
|
+
const value = String(rawMode || "").trim().toLowerCase();
|
|
224
|
+
if (INTERACTIVE_AGENT_MODES.includes(value)) return value;
|
|
225
|
+
if (options?.agentCategory === "voice") return "voice";
|
|
226
|
+
if (options?.agentCategory === "interactive") return "agent";
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function resolveAgentProfileLibraryMetadata(entry, profile = {}) {
|
|
231
|
+
const agentType = normalizeAgentProfileType(profile?.agentType, {
|
|
232
|
+
voiceAgent: profile?.voiceAgent === true,
|
|
233
|
+
});
|
|
234
|
+
const agentCategory = normalizeAgentLibraryCategory(profile?.agentCategory, {
|
|
235
|
+
agentType,
|
|
236
|
+
voiceAgent: profile?.voiceAgent === true,
|
|
237
|
+
});
|
|
238
|
+
const interactiveMode = normalizeInteractiveAgentMode(
|
|
239
|
+
profile?.interactiveMode || profile?.chatMode,
|
|
240
|
+
{ agentCategory },
|
|
241
|
+
);
|
|
242
|
+
const interactiveLabel = String(profile?.interactiveLabel || "").trim();
|
|
243
|
+
const explicitDropdown = profile?.showInChatDropdown;
|
|
244
|
+
const showInChatDropdown = agentCategory === "interactive"
|
|
245
|
+
? explicitDropdown === true
|
|
246
|
+
: false;
|
|
247
|
+
return {
|
|
248
|
+
agentType,
|
|
249
|
+
agentCategory,
|
|
250
|
+
interactiveMode: interactiveMode || null,
|
|
251
|
+
interactiveLabel: interactiveLabel || null,
|
|
252
|
+
showInChatDropdown,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
203
255
|
|
|
204
256
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
205
257
|
|
|
@@ -497,10 +549,11 @@ function updateSkillEntryIndexCache(rootDir, index, manifestMtimeMs = 0) {
|
|
|
497
549
|
function buildIndexedAgentProfile(rootDir, entry) {
|
|
498
550
|
const profile = getEntryContent(rootDir, entry);
|
|
499
551
|
if (!profile || typeof profile !== "object") return null;
|
|
552
|
+
const metadata = resolveAgentProfileLibraryMetadata(entry, profile);
|
|
500
553
|
return {
|
|
501
554
|
...entry,
|
|
502
555
|
profile,
|
|
503
|
-
|
|
556
|
+
...metadata,
|
|
504
557
|
titlePatterns: toStringArray(profile?.titlePatterns),
|
|
505
558
|
scopes: toStringArray(profile?.scopes),
|
|
506
559
|
tags: uniqueStrings([...(entry?.tags || []), ...toStringArray(profile?.tags)]),
|
|
@@ -1129,6 +1182,10 @@ export function resolveLibraryPlan(rootDir, criteria = {}, opts = {}) {
|
|
|
1129
1182
|
* @property {Object} [env] - extra env vars for the agent
|
|
1130
1183
|
* @property {string[]} [enabledTools] - list of tool IDs enabled for this agent (null = all)
|
|
1131
1184
|
* @property {string[]} [enabledMcpServers] - list of MCP server IDs enabled for this agent
|
|
1185
|
+
* @property {"task"|"interactive"|"voice"} [agentCategory] - library grouping/category for the profile
|
|
1186
|
+
* @property {"ask"|"agent"|"plan"|"web"|"instant"|"custom"|"voice"} [interactiveMode] - preferred manual interaction mode
|
|
1187
|
+
* @property {string} [interactiveLabel] - custom label shown for manual agent type/grouping
|
|
1188
|
+
* @property {boolean} [showInChatDropdown] - whether this interactive profile is shown in the chat manual-agent dropdown
|
|
1132
1189
|
*/
|
|
1133
1190
|
|
|
1134
1191
|
/**
|
package/infra/maintenance.mjs
CHANGED
|
@@ -1355,7 +1355,7 @@ export async function runMaintenanceSweep(opts = {}) {
|
|
|
1355
1355
|
|
|
1356
1356
|
// Guard against core.bare=true corruption that accumulates from worktree ops
|
|
1357
1357
|
try {
|
|
1358
|
-
const repoRoot = resolve(import.meta.dirname || ".", ".."
|
|
1358
|
+
const repoRoot = resolve(import.meta.dirname || ".", "..");
|
|
1359
1359
|
fixGitConfigCorruption(repoRoot);
|
|
1360
1360
|
} catch {
|
|
1361
1361
|
/* best-effort */
|
|
@@ -1389,4 +1389,3 @@ export async function runMaintenanceSweep(opts = {}) {
|
|
|
1389
1389
|
|
|
1390
1390
|
return result;
|
|
1391
1391
|
}
|
|
1392
|
-
|
package/infra/monitor.mjs
CHANGED
|
@@ -510,17 +510,24 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
const kanbanService = {
|
|
513
|
-
createTask: async (
|
|
513
|
+
createTask: async (projectIdOrTaskData = {}, taskDataArg = undefined) => {
|
|
514
|
+
const invokedWithProjectId = typeof projectIdOrTaskData === "string";
|
|
515
|
+
const payloadCandidate = invokedWithProjectId ? taskDataArg : projectIdOrTaskData;
|
|
516
|
+
const payload =
|
|
517
|
+
payloadCandidate && typeof payloadCandidate === "object" && !Array.isArray(payloadCandidate)
|
|
518
|
+
? { ...payloadCandidate }
|
|
519
|
+
: {};
|
|
514
520
|
const backend = getActiveKanbanBackend();
|
|
515
521
|
const projectId = String(
|
|
516
|
-
|
|
522
|
+
(invokedWithProjectId ? projectIdOrTaskData : payload?.projectId) ||
|
|
523
|
+
getConfiguredKanbanProjectId(backend) ||
|
|
524
|
+
"",
|
|
517
525
|
).trim();
|
|
518
526
|
if (!projectId) {
|
|
519
527
|
throw new Error(
|
|
520
528
|
`No project ID configured for backend=${backend} (required for workflow action.create_task)`,
|
|
521
529
|
);
|
|
522
530
|
}
|
|
523
|
-
const payload = { ...(taskData || {}) };
|
|
524
531
|
delete payload.projectId;
|
|
525
532
|
return createTask(projectId, payload);
|
|
526
533
|
},
|
|
@@ -1720,8 +1727,8 @@ configureExecutorTaskStatusTransitions();
|
|
|
1720
1727
|
}
|
|
1721
1728
|
}
|
|
1722
1729
|
|
|
1723
|
-
// Guard against core.bare=true corruption on the
|
|
1724
|
-
fixGitConfigCorruption(resolve(__dirname, ".."
|
|
1730
|
+
// Guard against core.bare=true corruption on the Bosun repo at startup.
|
|
1731
|
+
fixGitConfigCorruption(resolve(__dirname, ".."));
|
|
1725
1732
|
|
|
1726
1733
|
function canSignalProcess(pid) {
|
|
1727
1734
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
@@ -13284,8 +13291,15 @@ async function startProcess() {
|
|
|
13284
13291
|
.catch((err) =>
|
|
13285
13292
|
console.warn(
|
|
13286
13293
|
`[workspace-monitor] failed to start for ${shortId}: ${err.message}`,
|
|
13287
|
-
|
|
13288
|
-
|
|
13294
|
+
),
|
|
13295
|
+
);
|
|
13296
|
+
}
|
|
13297
|
+
if (
|
|
13298
|
+
typeof engine.load === "function" &&
|
|
13299
|
+
((Array.isArray(reconcile?.updatedWorkflowIds) && reconcile.updatedWorkflowIds.length > 0) ||
|
|
13300
|
+
Number(reconcile?.metadataUpdated || 0) > 0)
|
|
13301
|
+
) {
|
|
13302
|
+
engine.load();
|
|
13289
13303
|
}
|
|
13290
13304
|
}
|
|
13291
13305
|
|
|
@@ -15509,6 +15523,8 @@ if (isExecutorDisabled()) {
|
|
|
15509
15523
|
getTask: (taskId) => getInternalTask(taskId),
|
|
15510
15524
|
setTaskStatus: (taskId, status, source) =>
|
|
15511
15525
|
setInternalTaskStatus(taskId, status, source),
|
|
15526
|
+
updateTask: (taskId, updates) =>
|
|
15527
|
+
updateInternalTask(taskId, updates),
|
|
15512
15528
|
// broadcastUiEvent is wired later when UI server starts via
|
|
15513
15529
|
// injectUiDependencies → setBroadcastFn pattern
|
|
15514
15530
|
});
|
|
@@ -15534,6 +15550,8 @@ if (isExecutorDisabled()) {
|
|
|
15534
15550
|
getTask: (taskId) => getInternalTask(taskId),
|
|
15535
15551
|
setTaskStatus: (taskId, status, source) =>
|
|
15536
15552
|
setInternalTaskStatus(taskId, status, source),
|
|
15553
|
+
updateTask: (taskId, updates) =>
|
|
15554
|
+
updateInternalTask(taskId, updates),
|
|
15537
15555
|
assessIntervalMs: 30_000,
|
|
15538
15556
|
// ── Intervention callbacks (steering, thread management) ──
|
|
15539
15557
|
forceNewThread: (taskId, reason) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
15
|
-
import { resolve, dirname } from "node:path";
|
|
15
|
+
import { resolve, dirname, sep } from "node:path";
|
|
16
16
|
import { randomBytes } from "node:crypto";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { buildSessionInsights } from "../lib/session-insights.mjs";
|
|
@@ -20,7 +20,20 @@ import { isTestRuntime } from "./test-runtime.mjs";
|
|
|
20
20
|
import { addCompletedSession } from "./runtime-accumulator.mjs";
|
|
21
21
|
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const
|
|
23
|
+
const WORKSPACE_MIRROR_MARKER = `${sep}.bosun${sep}workspaces${sep}`.toLowerCase();
|
|
24
|
+
|
|
25
|
+
function resolveSessionTrackerSourceRepoRoot(startDir = __dirname) {
|
|
26
|
+
const normalized = resolve(startDir);
|
|
27
|
+
const lower = normalized.toLowerCase();
|
|
28
|
+
const mirrorIndex = lower.indexOf(WORKSPACE_MIRROR_MARKER);
|
|
29
|
+
if (mirrorIndex >= 0) {
|
|
30
|
+
return normalized.slice(0, mirrorIndex);
|
|
31
|
+
}
|
|
32
|
+
return resolve(normalized, "..");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SESSION_TRACKER_REPO_ROOT = resolveSessionTrackerSourceRepoRoot(__dirname);
|
|
36
|
+
const SESSIONS_DIR = resolve(SESSION_TRACKER_REPO_ROOT, "logs", "sessions");
|
|
24
37
|
|
|
25
38
|
const TAG = "[session-tracker]";
|
|
26
39
|
|
|
@@ -56,6 +69,11 @@ function resolveSessionTrackerPersistDir(options = {}) {
|
|
|
56
69
|
return isTestRuntime() ? null : SESSIONS_DIR;
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
export const _test = Object.freeze({
|
|
73
|
+
resolveSessionTrackerSourceRepoRoot,
|
|
74
|
+
resolveSessionTrackerPersistDir,
|
|
75
|
+
});
|
|
76
|
+
|
|
59
77
|
function resolveSessionMaxMessages(type, metadata, explicitMax, fallbackMax) {
|
|
60
78
|
if (Number.isFinite(explicitMax)) {
|
|
61
79
|
return explicitMax > 0 ? explicitMax : 0;
|
|
@@ -575,18 +593,27 @@ export class SessionTracker {
|
|
|
575
593
|
listAllSessions() {
|
|
576
594
|
const list = [];
|
|
577
595
|
for (const s of this.#sessions.values()) {
|
|
596
|
+
const progress = s.status === "active"
|
|
597
|
+
? this.getProgressStatus(s.id || s.taskId)
|
|
598
|
+
: null;
|
|
599
|
+
const derivedStatus = progress?.status === "ended"
|
|
600
|
+
? "completed"
|
|
601
|
+
: (progress?.status || s.status);
|
|
578
602
|
list.push({
|
|
579
603
|
id: s.id || s.taskId,
|
|
580
604
|
taskId: s.taskId,
|
|
581
605
|
title: s.taskTitle || s.title || null,
|
|
582
606
|
type: s.type || "task",
|
|
583
|
-
status:
|
|
607
|
+
status: derivedStatus,
|
|
584
608
|
workspaceId: String(s?.metadata?.workspaceId || "").trim() || null,
|
|
585
609
|
workspaceDir: String(s?.metadata?.workspaceDir || "").trim() || null,
|
|
586
610
|
branch: String(s?.metadata?.branch || "").trim() || null,
|
|
587
611
|
turnCount: s.turnCount || 0,
|
|
588
612
|
createdAt: s.createdAt || new Date(s.startedAt).toISOString(),
|
|
589
613
|
lastActiveAt: s.lastActiveAt || new Date(s.lastActivityAt).toISOString(),
|
|
614
|
+
idleMs: progress?.idleMs ?? 0,
|
|
615
|
+
elapsedMs: progress?.elapsedMs ?? Math.max(0, Date.now() - Number(s.startedAt || Date.now())),
|
|
616
|
+
recommendation: progress?.recommendation || "none",
|
|
590
617
|
preview: this.#lastMessagePreview(s),
|
|
591
618
|
lastMessage: this.#lastMessagePreview(s),
|
|
592
619
|
insights: s.insights || null,
|