bosun 0.37.1 → 0.37.2
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 +4 -1
- package/agent-tool-config.mjs +14 -3
- package/bosun-skills.mjs +59 -4
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +48 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +2 -1
- package/setup-web-server.mjs +71 -10
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +110 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +3 -0
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +260 -38
- package/ui/modules/voice-client.js +662 -58
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +219 -9
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +22 -5
- package/ui-server.mjs +961 -103
- package/voice-relay.mjs +119 -11
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/.env.example
CHANGED
|
@@ -21,7 +21,6 @@ SHARED_STATE_STALE_THRESHOLD_MS=300000
|
|
|
21
21
|
# Maximum retry attempts before permanently ignoring a task (default: 3)
|
|
22
22
|
SHARED_STATE_MAX_RETRIES=3
|
|
23
23
|
# Task claim owner staleness threshold in milliseconds (default: 600000 = 10 minutes)
|
|
24
|
-
# Used by task-claims.mjs to detect stale local claims
|
|
25
24
|
TASK_CLAIM_OWNER_STALE_TTL_MS=600000
|
|
26
25
|
|
|
27
26
|
# ─── Project Identity ─────────────────────────────────────────────────────────
|
|
@@ -178,6 +177,10 @@ VOICE_VISION_MODEL=gpt-4.1-mini
|
|
|
178
177
|
# Gemini provider mode (Tier 2 voice fallback + Gemini vision)
|
|
179
178
|
# GEMINI_API_KEY=
|
|
180
179
|
# GOOGLE_API_KEY=
|
|
180
|
+
# Transcription model used for audio-to-text (default: gpt-4o-transcribe)
|
|
181
|
+
# VOICE_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe
|
|
182
|
+
# Enable/disable input audio transcription in realtime sessions (default: true)
|
|
183
|
+
# VOICE_TRANSCRIPTION_ENABLED=true
|
|
181
184
|
# Voice output persona
|
|
182
185
|
VOICE_ID=alloy
|
|
183
186
|
# server_vad | semantic_vad | none
|
package/agent-tool-config.mjs
CHANGED
|
@@ -286,12 +286,23 @@ export function getEffectiveTools(rootDir, agentId) {
|
|
|
286
286
|
const agentConfig = config.agents[agentId] || {};
|
|
287
287
|
const disabledSet = new Set(agentConfig.disabledBuiltinTools || []);
|
|
288
288
|
const defaultIds = new Set(config.defaults?.builtinTools || DEFAULT_BUILTIN_TOOLS.filter((t) => t.default).map((t) => t.id));
|
|
289
|
+
const builtinIdSet = new Set(DEFAULT_BUILTIN_TOOLS.map((tool) => tool.id));
|
|
290
|
+
const explicitEnabled = Array.isArray(agentConfig.enabledTools)
|
|
291
|
+
? agentConfig.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
292
|
+
: null;
|
|
293
|
+
const explicitBuiltinEnabled = explicitEnabled
|
|
294
|
+
? explicitEnabled.filter((id) => builtinIdSet.has(id))
|
|
295
|
+
: [];
|
|
296
|
+
const useBuiltinAllowlist = explicitBuiltinEnabled.length > 0;
|
|
297
|
+
const explicitBuiltinSet = new Set(explicitBuiltinEnabled);
|
|
289
298
|
|
|
290
299
|
const builtinTools = DEFAULT_BUILTIN_TOOLS.map((tool) => ({
|
|
291
300
|
...tool,
|
|
292
|
-
enabled: !disabledSet.has(tool.id) && (
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
enabled: !disabledSet.has(tool.id) && (
|
|
302
|
+
useBuiltinAllowlist
|
|
303
|
+
? explicitBuiltinSet.has(tool.id)
|
|
304
|
+
: defaultIds.has(tool.id)
|
|
305
|
+
),
|
|
295
306
|
}));
|
|
296
307
|
|
|
297
308
|
return {
|
package/bosun-skills.mjs
CHANGED
|
@@ -16,8 +16,44 @@
|
|
|
16
16
|
* scan quickly to decide which skill files to read.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import { resolve, basename } from "node:path";
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve, basename } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
// ── Analytics stream path (same file task-executor writes to) ────────────────
|
|
27
|
+
const _SKILL_STREAM_PATH = resolve(
|
|
28
|
+
__dirname,
|
|
29
|
+
".cache",
|
|
30
|
+
"agent-work-logs",
|
|
31
|
+
"agent-work-stream.jsonl",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort: emit a skill_invoke event to the agent work stream so usage
|
|
36
|
+
* analytics can track which skills are loaded per task.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} skillName
|
|
39
|
+
* @param {string} [skillTitle]
|
|
40
|
+
* @param {{ taskId?: string, executor?: string }} [opts]
|
|
41
|
+
*/
|
|
42
|
+
function emitSkillInvokeEvent(skillName, skillTitle, opts = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const event = {
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
event_type: "skill_invoke",
|
|
47
|
+
data: { skill_name: skillName, skill_title: skillTitle || skillName },
|
|
48
|
+
...(opts.taskId ? { task_id: String(opts.taskId) } : {}),
|
|
49
|
+
...(opts.executor ? { executor: String(opts.executor) } : {}),
|
|
50
|
+
};
|
|
51
|
+
mkdirSync(dirname(_SKILL_STREAM_PATH), { recursive: true });
|
|
52
|
+
appendFileSync(_SKILL_STREAM_PATH, JSON.stringify(event) + "\n", "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
/* best effort — never let analytics crash skill loading */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
21
57
|
|
|
22
58
|
// ── Built-in skill definitions ────────────────────────────────────────────────
|
|
23
59
|
|
|
@@ -911,14 +947,25 @@ export function loadSkillsIndex(bosunHome) {
|
|
|
911
947
|
* @param {string} [taskDescription]
|
|
912
948
|
* @returns {Array<{filename:string,title:string,tags:string[],content:string}>}
|
|
913
949
|
*/
|
|
914
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Find skills relevant to a given task by matching tags against the task title
|
|
952
|
+
* and description. Also emits `skill_invoke` analytics events for each matched
|
|
953
|
+
* skill so usage analytics can track skill popularity over time.
|
|
954
|
+
*
|
|
955
|
+
* @param {string} bosunHome
|
|
956
|
+
* @param {string} taskTitle
|
|
957
|
+
* @param {string} [taskDescription]
|
|
958
|
+
* @param {{ taskId?: string, executor?: string }} [opts] - Optional task context for analytics.
|
|
959
|
+
* @returns {Array<{filename:string,title:string,tags:string[],content:string}>}
|
|
960
|
+
*/
|
|
961
|
+
export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "", opts = {}) {
|
|
915
962
|
const index = loadSkillsIndex(bosunHome);
|
|
916
963
|
if (!index?.skills?.length) return [];
|
|
917
964
|
|
|
918
965
|
const searchText = `${taskTitle} ${taskDescription}`.toLowerCase();
|
|
919
966
|
const skillsDir = getSkillsDir(bosunHome);
|
|
920
967
|
|
|
921
|
-
|
|
968
|
+
const matched = index.skills
|
|
922
969
|
.filter(({ tags }) =>
|
|
923
970
|
tags.some((tag) => searchText.includes(tag)),
|
|
924
971
|
)
|
|
@@ -930,4 +977,12 @@ export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "") {
|
|
|
930
977
|
return { filename, title, tags, content };
|
|
931
978
|
})
|
|
932
979
|
.filter(({ content }) => !!content);
|
|
980
|
+
|
|
981
|
+
// Emit analytics events for each loaded skill
|
|
982
|
+
for (const skill of matched) {
|
|
983
|
+
const skillName = skill.filename.replace(/\.md$/i, "");
|
|
984
|
+
emitSkillInvokeEvent(skillName, skill.title, opts);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return matched;
|
|
933
988
|
}
|
package/desktop/launch.mjs
CHANGED
|
@@ -17,6 +17,13 @@ const chromeSandbox = resolve(
|
|
|
17
17
|
|
|
18
18
|
process.title = "bosun-desktop-launcher";
|
|
19
19
|
|
|
20
|
+
function hasGuiEnvironment() {
|
|
21
|
+
if (process.platform !== "linux") return true;
|
|
22
|
+
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return true;
|
|
23
|
+
if (process.env.XDG_SESSION_TYPE && process.env.XDG_SESSION_TYPE !== "tty") return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
function shouldDisableSandbox() {
|
|
21
28
|
if (process.env.BOSUN_DESKTOP_DISABLE_SANDBOX === "1") return true;
|
|
22
29
|
if (process.platform !== "linux") return false;
|
|
@@ -49,6 +56,17 @@ function ensureElectronInstalled() {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
function launch() {
|
|
59
|
+
if (!hasGuiEnvironment()) {
|
|
60
|
+
console.error(
|
|
61
|
+
[
|
|
62
|
+
"[desktop] No GUI display server detected.",
|
|
63
|
+
"Cannot launch Electron portal without DISPLAY/WAYLAND.",
|
|
64
|
+
"Run Bosun in daemon/web mode instead (for example: `bosun --daemon`).",
|
|
65
|
+
].join(" "),
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
if (!ensureElectronInstalled()) {
|
|
53
71
|
process.exit(1);
|
|
54
72
|
}
|
package/desktop/main.mjs
CHANGED
|
@@ -89,6 +89,7 @@ let shuttingDown = false;
|
|
|
89
89
|
let uiServerStarted = false;
|
|
90
90
|
let uiOrigin = null;
|
|
91
91
|
let uiApi = null;
|
|
92
|
+
let desktopAuthHeaderBridgeInstalled = false;
|
|
92
93
|
let runtimeConfigLoaded = false;
|
|
93
94
|
/** True when the app is running as a persistent background / tray resident. */
|
|
94
95
|
let trayMode = false;
|
|
@@ -154,6 +155,38 @@ function isTrustedCaptureOrigin(originLike) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
function isTrustedDesktopRequestUrl(urlLike) {
|
|
159
|
+
return isTrustedCaptureOrigin(urlLike);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function installDesktopAuthHeaderBridge() {
|
|
163
|
+
if (desktopAuthHeaderBridgeInstalled) return;
|
|
164
|
+
const ses = session.defaultSession;
|
|
165
|
+
if (!ses) return;
|
|
166
|
+
ses.webRequest.onBeforeSendHeaders((details, callback) => {
|
|
167
|
+
try {
|
|
168
|
+
const desktopKey = String(process.env.BOSUN_DESKTOP_API_KEY || "").trim();
|
|
169
|
+
if (!desktopKey) {
|
|
170
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!isTrustedDesktopRequestUrl(details?.url || "")) {
|
|
174
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const headers = { ...(details.requestHeaders || {}) };
|
|
178
|
+
const existingAuth = String(headers.Authorization || headers.authorization || "").trim();
|
|
179
|
+
if (!existingAuth) {
|
|
180
|
+
headers.Authorization = `Bearer ${desktopKey}`;
|
|
181
|
+
}
|
|
182
|
+
callback({ requestHeaders: headers });
|
|
183
|
+
} catch {
|
|
184
|
+
callback({ requestHeaders: details.requestHeaders });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
desktopAuthHeaderBridgeInstalled = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
157
190
|
function installDesktopMediaHandlers() {
|
|
158
191
|
const ses = session.defaultSession;
|
|
159
192
|
if (!ses) return;
|
|
@@ -583,20 +616,25 @@ async function fetchWorkspaces({ force = false } = {}) {
|
|
|
583
616
|
* @param {string} workspaceId
|
|
584
617
|
*/
|
|
585
618
|
async function switchWorkspace(workspaceId) {
|
|
586
|
-
if (!uiOrigin || !workspaceId) return;
|
|
619
|
+
if (!uiOrigin || !workspaceId) return { ok: false, error: "workspace unavailable" };
|
|
587
620
|
const body = JSON.stringify({ workspaceId });
|
|
588
621
|
const data = await uiServerRequest(`${uiOrigin}/api/workspaces/active`, {
|
|
589
622
|
method: "POST",
|
|
590
623
|
body,
|
|
591
624
|
});
|
|
592
|
-
if (data?.ok) {
|
|
593
|
-
|
|
594
|
-
|
|
625
|
+
if (!data?.ok) {
|
|
626
|
+
return {
|
|
627
|
+
ok: false,
|
|
628
|
+
error: String(data?.error || "workspace switch failed"),
|
|
629
|
+
};
|
|
595
630
|
}
|
|
631
|
+
_cachedActiveWorkspaceId = String(data?.activeId || workspaceId);
|
|
632
|
+
_workspaceCacheAt = 0; // force re-fetch next time
|
|
596
633
|
await fetchWorkspaces({ force: true });
|
|
597
634
|
Menu.setApplicationMenu(buildAppMenu());
|
|
598
635
|
refreshTrayMenu();
|
|
599
636
|
navigateMainWindow("/");
|
|
637
|
+
return { ok: true, activeId: _cachedActiveWorkspaceId };
|
|
600
638
|
}
|
|
601
639
|
|
|
602
640
|
/**
|
|
@@ -1265,14 +1303,15 @@ async function openFollowWindow(detail = {}) {
|
|
|
1265
1303
|
const win = await createFollowWindow();
|
|
1266
1304
|
const baseUiUrl = await buildUiUrl();
|
|
1267
1305
|
const target = buildFollowWindowUrl(baseUiUrl, detail);
|
|
1306
|
+
// Append a cache-buster timestamp so every Call press produces a unique URL.
|
|
1307
|
+
// Without this, a second Call press with the same parameters would match
|
|
1308
|
+
// followWindowLaunchSignature and skip loadURL — leaving the follow window
|
|
1309
|
+
// in its previous dead state (launch params already scrubbed, voice overlay
|
|
1310
|
+
// closed, useEffect([], []) already fired and won't re-run).
|
|
1311
|
+
target.searchParams.set("t", String(Date.now()));
|
|
1268
1312
|
const signature = target.toString();
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
await win.loadURL(signature);
|
|
1272
|
-
anchorFollowWindow(win);
|
|
1273
|
-
setWindowVisible(win);
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1313
|
+
followWindowLaunchSignature = signature;
|
|
1314
|
+
await win.loadURL(signature);
|
|
1276
1315
|
anchorFollowWindow(win);
|
|
1277
1316
|
setWindowVisible(win);
|
|
1278
1317
|
}
|
|
@@ -1727,8 +1766,7 @@ function registerDesktopIpc() {
|
|
|
1727
1766
|
*/
|
|
1728
1767
|
ipcMain.handle("bosun:workspaces:switch", async (_event, { workspaceId } = {}) => {
|
|
1729
1768
|
if (!workspaceId) return { ok: false, error: "workspaceId required" };
|
|
1730
|
-
|
|
1731
|
-
return { ok: true, activeId: workspaceId };
|
|
1769
|
+
return switchWorkspace(workspaceId);
|
|
1732
1770
|
});
|
|
1733
1771
|
}
|
|
1734
1772
|
|
|
@@ -1745,6 +1783,7 @@ async function bootstrap() {
|
|
|
1745
1783
|
}
|
|
1746
1784
|
callback(-3); // -3 = use Chromium default chain verification
|
|
1747
1785
|
});
|
|
1786
|
+
installDesktopAuthHeaderBridge();
|
|
1748
1787
|
installDesktopMediaHandlers();
|
|
1749
1788
|
|
|
1750
1789
|
if (process.env.ELECTRON_DISABLE_SANDBOX === "1") {
|
package/fleet-coordinator.mjs
CHANGED
|
@@ -36,6 +36,24 @@ import {
|
|
|
36
36
|
|
|
37
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
38
|
|
|
39
|
+
// ── Workflow Event Bridge ────────────────────────────────────────────────────
|
|
40
|
+
// Lazy-import queueWorkflowEvent from monitor.mjs — cached at module scope.
|
|
41
|
+
let _queueWorkflowEvent = null;
|
|
42
|
+
function emitFleetEvent(eventType, eventData = {}, opts = {}) {
|
|
43
|
+
if (!_queueWorkflowEvent) {
|
|
44
|
+
import("./monitor.mjs")
|
|
45
|
+
.then((mod) => {
|
|
46
|
+
if (typeof mod.queueWorkflowEvent === "function") {
|
|
47
|
+
_queueWorkflowEvent = mod.queueWorkflowEvent;
|
|
48
|
+
_queueWorkflowEvent(eventType, eventData, opts);
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
.catch(() => {});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_queueWorkflowEvent(eventType, eventData, opts);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
// ── Repo Fingerprinting ──────────────────────────────────────────────────────
|
|
40
58
|
|
|
41
59
|
function buildGitEnv() {
|
|
@@ -434,13 +452,21 @@ export function assignTasksToWorkstations(waves, peers, taskMap = new Map()) {
|
|
|
434
452
|
assignments.push(...waveAssignments);
|
|
435
453
|
}
|
|
436
454
|
|
|
437
|
-
|
|
455
|
+
const result = {
|
|
438
456
|
assignments,
|
|
439
457
|
totalTasks: assignments.length,
|
|
440
458
|
totalPeers: peers.length,
|
|
441
459
|
waveCount: waves.length,
|
|
442
460
|
createdAt: new Date().toISOString(),
|
|
443
461
|
};
|
|
462
|
+
|
|
463
|
+
emitFleetEvent("fleet.tasks_assigned", {
|
|
464
|
+
totalTasks: result.totalTasks,
|
|
465
|
+
totalPeers: result.totalPeers,
|
|
466
|
+
waveCount: result.waveCount,
|
|
467
|
+
}, { dedupKey: `fleet-assign-${result.createdAt}` });
|
|
468
|
+
|
|
469
|
+
return result;
|
|
444
470
|
}
|
|
445
471
|
|
|
446
472
|
// ── Backlog Depth Calculator ─────────────────────────────────────────────────
|
|
@@ -502,6 +528,10 @@ export function detectMaintenanceMode(status) {
|
|
|
502
528
|
|
|
503
529
|
// Maintenance mode: nothing to do AND nothing in progress
|
|
504
530
|
if (backlog === 0 && todo === 0 && running === 0 && review === 0) {
|
|
531
|
+
emitFleetEvent("fleet.maintenance_mode", {
|
|
532
|
+
isMaintenanceMode: true,
|
|
533
|
+
reason: "all tasks completed — no backlog, no active work",
|
|
534
|
+
});
|
|
505
535
|
return {
|
|
506
536
|
isMaintenanceMode: true,
|
|
507
537
|
reason: "all tasks completed — no backlog, no active work",
|
|
@@ -722,6 +752,9 @@ export function shouldAutoGenerateTasks({
|
|
|
722
752
|
*/
|
|
723
753
|
export function markAutoGenTriggered() {
|
|
724
754
|
lastAutoGenTimestamp = Date.now();
|
|
755
|
+
emitFleetEvent("fleet.auto_gen_triggered", {
|
|
756
|
+
triggeredAt: new Date(lastAutoGenTimestamp).toISOString(),
|
|
757
|
+
});
|
|
725
758
|
}
|
|
726
759
|
|
|
727
760
|
/**
|
package/kanban-adapter.mjs
CHANGED
|
@@ -5823,6 +5823,23 @@ export function getKanbanBackendName() {
|
|
|
5823
5823
|
// Convenience exports: direct task operations via active adapter
|
|
5824
5824
|
// ---------------------------------------------------------------------------
|
|
5825
5825
|
|
|
5826
|
+
// ── Workflow Event Bridge (lazy-loaded from monitor.mjs) ──────────────────
|
|
5827
|
+
let _kanbanQueueWorkflowEvent = null;
|
|
5828
|
+
function emitKanbanEvent(eventType, eventData = {}) {
|
|
5829
|
+
if (!_kanbanQueueWorkflowEvent) {
|
|
5830
|
+
import("./monitor.mjs")
|
|
5831
|
+
.then((mod) => {
|
|
5832
|
+
if (typeof mod.queueWorkflowEvent === "function") {
|
|
5833
|
+
_kanbanQueueWorkflowEvent = mod.queueWorkflowEvent;
|
|
5834
|
+
_kanbanQueueWorkflowEvent(eventType, eventData);
|
|
5835
|
+
}
|
|
5836
|
+
})
|
|
5837
|
+
.catch(() => {});
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
_kanbanQueueWorkflowEvent(eventType, eventData);
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5826
5843
|
export async function listProjects() {
|
|
5827
5844
|
return getKanbanAdapter().listProjects();
|
|
5828
5845
|
}
|
|
@@ -5836,7 +5853,9 @@ export async function getTask(taskId) {
|
|
|
5836
5853
|
}
|
|
5837
5854
|
|
|
5838
5855
|
export async function updateTaskStatus(taskId, status, options) {
|
|
5839
|
-
|
|
5856
|
+
const result = await getKanbanAdapter().updateTaskStatus(taskId, status, options);
|
|
5857
|
+
emitKanbanEvent("task.status_updated", { taskId, status, options });
|
|
5858
|
+
return result;
|
|
5840
5859
|
}
|
|
5841
5860
|
|
|
5842
5861
|
export async function updateTask(taskId, patch) {
|
|
@@ -5851,11 +5870,19 @@ export async function updateTask(taskId, patch) {
|
|
|
5851
5870
|
}
|
|
5852
5871
|
|
|
5853
5872
|
export async function createTask(projectId, taskData) {
|
|
5854
|
-
|
|
5873
|
+
const result = await getKanbanAdapter().createTask(projectId, taskData);
|
|
5874
|
+
emitKanbanEvent("task.created", {
|
|
5875
|
+
projectId,
|
|
5876
|
+
taskId: result?.id || null,
|
|
5877
|
+
title: taskData?.title || null,
|
|
5878
|
+
});
|
|
5879
|
+
return result;
|
|
5855
5880
|
}
|
|
5856
5881
|
|
|
5857
5882
|
export async function deleteTask(taskId) {
|
|
5858
|
-
|
|
5883
|
+
const result = await getKanbanAdapter().deleteTask(taskId);
|
|
5884
|
+
emitKanbanEvent("task.deleted", { taskId });
|
|
5885
|
+
return result;
|
|
5859
5886
|
}
|
|
5860
5887
|
|
|
5861
5888
|
export async function addComment(taskId, body) {
|
package/library-manager.mjs
CHANGED
|
@@ -633,6 +633,7 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
633
633
|
hookProfile: null,
|
|
634
634
|
env: {},
|
|
635
635
|
tags: ["ui", "frontend", "portal", "css", "web"],
|
|
636
|
+
agentType: "task",
|
|
636
637
|
},
|
|
637
638
|
{
|
|
638
639
|
id: "backend-agent",
|
|
@@ -647,6 +648,7 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
647
648
|
hookProfile: null,
|
|
648
649
|
env: {},
|
|
649
650
|
tags: ["api", "server", "backend", "database"],
|
|
651
|
+
agentType: "task",
|
|
650
652
|
},
|
|
651
653
|
{
|
|
652
654
|
id: "devops-agent",
|
|
@@ -661,6 +663,7 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
661
663
|
hookProfile: null,
|
|
662
664
|
env: {},
|
|
663
665
|
tags: ["ci", "cd", "build", "deploy", "infra", "devops"],
|
|
666
|
+
agentType: "task",
|
|
664
667
|
},
|
|
665
668
|
{
|
|
666
669
|
id: "docs-agent",
|
|
@@ -675,6 +678,7 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
675
678
|
hookProfile: null,
|
|
676
679
|
env: {},
|
|
677
680
|
tags: ["docs", "documentation", "readme", "markdown"],
|
|
681
|
+
agentType: "task",
|
|
678
682
|
},
|
|
679
683
|
{
|
|
680
684
|
id: "test-agent",
|
|
@@ -689,6 +693,47 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
689
693
|
hookProfile: null,
|
|
690
694
|
env: {},
|
|
691
695
|
tags: ["test", "testing", "e2e", "unit", "coverage"],
|
|
696
|
+
agentType: "task",
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "voice-agent-female",
|
|
700
|
+
name: "Voice Agent (Female)",
|
|
701
|
+
description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
|
|
702
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
703
|
+
scopes: ["voice", "assistant"],
|
|
704
|
+
sdk: null,
|
|
705
|
+
model: null,
|
|
706
|
+
promptOverride: null,
|
|
707
|
+
skills: ["concise-voice-guidance", "conversation-memory"],
|
|
708
|
+
hookProfile: null,
|
|
709
|
+
env: {},
|
|
710
|
+
tags: ["voice", "assistant", "realtime", "female", "default", "audio-agent"],
|
|
711
|
+
agentType: "voice",
|
|
712
|
+
voiceAgent: true,
|
|
713
|
+
voicePersona: "female",
|
|
714
|
+
voiceInstructions: "You are Nova, a female voice agent. Be concise, warm, and practical. Use tools for facts and execution. Keep spoken responses short and clear.",
|
|
715
|
+
enabledTools: null,
|
|
716
|
+
enabledMcpServers: [],
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
id: "voice-agent-male",
|
|
720
|
+
name: "Voice Agent (Male)",
|
|
721
|
+
description: "Operational voice specialist focused on diagnostics and execution.",
|
|
722
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
723
|
+
scopes: ["voice", "assistant"],
|
|
724
|
+
sdk: null,
|
|
725
|
+
model: null,
|
|
726
|
+
promptOverride: null,
|
|
727
|
+
skills: ["ops-diagnostics", "task-execution"],
|
|
728
|
+
hookProfile: null,
|
|
729
|
+
env: {},
|
|
730
|
+
tags: ["voice", "assistant", "realtime", "male", "default", "audio-agent"],
|
|
731
|
+
agentType: "voice",
|
|
732
|
+
voiceAgent: true,
|
|
733
|
+
voicePersona: "male",
|
|
734
|
+
voiceInstructions: "You are Atlas, a male voice agent. Be direct and execution-oriented. Prefer actionable status updates. Use tools proactively for diagnostics.",
|
|
735
|
+
enabledTools: null,
|
|
736
|
+
enabledMcpServers: [],
|
|
692
737
|
},
|
|
693
738
|
{
|
|
694
739
|
id: "voice-agent",
|
|
@@ -703,6 +748,9 @@ export const BUILTIN_AGENT_PROFILES = [
|
|
|
703
748
|
hookProfile: null,
|
|
704
749
|
env: {},
|
|
705
750
|
tags: ["voice", "assistant", "realtime", "default"],
|
|
751
|
+
agentType: "voice",
|
|
752
|
+
voiceAgent: true,
|
|
753
|
+
voicePersona: "neutral",
|
|
706
754
|
enabledTools: null,
|
|
707
755
|
enabledMcpServers: [],
|
|
708
756
|
},
|
package/maintenance.mjs
CHANGED
|
@@ -28,6 +28,26 @@ import {
|
|
|
28
28
|
} from "./worktree-manager.mjs";
|
|
29
29
|
|
|
30
30
|
const isWindows = process.platform === "win32";
|
|
31
|
+
|
|
32
|
+
// ── Workflow event bridge ─────────────────────────────────────────────────
|
|
33
|
+
// Lazy-loaded to avoid circular imports (monitor → maintenance → monitor).
|
|
34
|
+
let _queueWorkflowEvent = null;
|
|
35
|
+
function emitMaintenanceEvent(eventType, data = {}) {
|
|
36
|
+
if (_queueWorkflowEvent) {
|
|
37
|
+
_queueWorkflowEvent(eventType, data);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
import("./monitor.mjs")
|
|
41
|
+
.then((mod) => {
|
|
42
|
+
if (typeof mod.queueWorkflowEvent === "function") {
|
|
43
|
+
_queueWorkflowEvent = mod.queueWorkflowEvent;
|
|
44
|
+
_queueWorkflowEvent(eventType, data);
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
/* best-effort — monitor not available during tests */
|
|
49
|
+
});
|
|
50
|
+
}
|
|
31
51
|
const BRANCH_SYNC_LOG_THROTTLE_MS = Math.max(
|
|
32
52
|
5_000,
|
|
33
53
|
Number(process.env.BRANCH_SYNC_LOG_THROTTLE_MS || "300000") || 300000,
|
|
@@ -1318,11 +1338,7 @@ export async function runMaintenanceSweep(opts = {}) {
|
|
|
1318
1338
|
/* best-effort */
|
|
1319
1339
|
}
|
|
1320
1340
|
|
|
1321
|
-
|
|
1322
|
-
`[maintenance] sweep complete: ${staleKilled} stale orchestrators, ${pushesReaped} stuck pushes, ${worktreesPruned} worktrees pruned, ${branchesSynced} branches synced, ${branchesDeleted} stale branches deleted`,
|
|
1323
|
-
);
|
|
1324
|
-
|
|
1325
|
-
return {
|
|
1341
|
+
const result = {
|
|
1326
1342
|
staleKilled,
|
|
1327
1343
|
pushesReaped,
|
|
1328
1344
|
worktreesPruned,
|
|
@@ -1330,4 +1346,13 @@ export async function runMaintenanceSweep(opts = {}) {
|
|
|
1330
1346
|
tasksArchived,
|
|
1331
1347
|
branchesDeleted,
|
|
1332
1348
|
};
|
|
1349
|
+
|
|
1350
|
+
console.log(
|
|
1351
|
+
`[maintenance] sweep complete: ${staleKilled} stale orchestrators, ${pushesReaped} stuck pushes, ${worktreesPruned} worktrees pruned, ${branchesSynced} branches synced, ${branchesDeleted} stale branches deleted`,
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
// Emit workflow event so event-driven workflows can react to sweep results
|
|
1355
|
+
emitMaintenanceEvent("maintenance.sweep_complete", result);
|
|
1356
|
+
|
|
1357
|
+
return result;
|
|
1333
1358
|
}
|
package/monitor.mjs
CHANGED
|
@@ -12235,12 +12235,66 @@ runGuarded("startup-maintenance-sweep", () =>
|
|
|
12235
12235
|
safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
|
|
12236
12236
|
|
|
12237
12237
|
// ── Periodic maintenance: every 5 min, reap stuck pushes & prune worktrees ──
|
|
12238
|
+
// If a workflow replaces maintenance.mjs, skip the direct call and let
|
|
12239
|
+
// the workflow engine handle it via trigger.schedule evaluation.
|
|
12238
12240
|
const maintenanceIntervalMs = 5 * 60 * 1000;
|
|
12239
12241
|
safeSetInterval("maintenance-sweep", () => {
|
|
12242
|
+
if (isWorkflowReplacingModule("maintenance.mjs")) return;
|
|
12240
12243
|
const childPid = currentChild ? currentChild.pid : undefined;
|
|
12241
12244
|
return runMaintenanceSweep({ repoRoot, childPid });
|
|
12242
12245
|
}, maintenanceIntervalMs);
|
|
12243
12246
|
|
|
12247
|
+
// ── Workflow schedule trigger polling ───────────────────────────────────────
|
|
12248
|
+
// Check all installed workflows that use trigger.schedule and fire any whose
|
|
12249
|
+
// interval has elapsed. This makes schedule-based templates (workspace hygiene,
|
|
12250
|
+
// nightly reports, etc.) actually execute without hardcoded safeSetInterval calls.
|
|
12251
|
+
const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
|
|
12252
|
+
safeSetInterval("workflow-schedule-check", async () => {
|
|
12253
|
+
try {
|
|
12254
|
+
const engine = await ensureWorkflowAutomationEngine();
|
|
12255
|
+
if (!engine?.evaluateScheduleTriggers) return;
|
|
12256
|
+
|
|
12257
|
+
const triggered = engine.evaluateScheduleTriggers();
|
|
12258
|
+
if (!Array.isArray(triggered) || triggered.length === 0) return;
|
|
12259
|
+
|
|
12260
|
+
for (const match of triggered) {
|
|
12261
|
+
const workflowId = String(match?.workflowId || "").trim();
|
|
12262
|
+
if (!workflowId) continue;
|
|
12263
|
+
void engine
|
|
12264
|
+
.execute(workflowId, {
|
|
12265
|
+
_triggerSource: "schedule-poll",
|
|
12266
|
+
_triggeredBy: match?.triggeredBy || null,
|
|
12267
|
+
_lastRunAt: Date.now(),
|
|
12268
|
+
repoRoot,
|
|
12269
|
+
})
|
|
12270
|
+
.then((ctx) => {
|
|
12271
|
+
const runId = ctx?.id || "unknown";
|
|
12272
|
+
const runStatus =
|
|
12273
|
+
Array.isArray(ctx?.errors) && ctx.errors.length > 0
|
|
12274
|
+
? "failed"
|
|
12275
|
+
: "completed";
|
|
12276
|
+
console.log(
|
|
12277
|
+
`[workflows] schedule-run ${runStatus} workflow=${workflowId} runId=${runId}`,
|
|
12278
|
+
);
|
|
12279
|
+
})
|
|
12280
|
+
.catch((err) => {
|
|
12281
|
+
console.warn(
|
|
12282
|
+
`[workflows] schedule-run failed workflow=${workflowId}: ${err?.message || err}`,
|
|
12283
|
+
);
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
|
|
12287
|
+
if (triggered.length > 0) {
|
|
12288
|
+
console.log(
|
|
12289
|
+
`[workflows] schedule poll triggered ${triggered.length} workflow run(s)`,
|
|
12290
|
+
);
|
|
12291
|
+
}
|
|
12292
|
+
} catch (err) {
|
|
12293
|
+
// Schedule evaluation must not crash the monitor
|
|
12294
|
+
console.warn(`[workflows] schedule-check error: ${err?.message || err}`);
|
|
12295
|
+
}
|
|
12296
|
+
}, scheduleCheckIntervalMs);
|
|
12297
|
+
|
|
12244
12298
|
// ── Periodic merged PR check: every 10 min, move merged PRs to done ─────────
|
|
12245
12299
|
const mergedPRCheckIntervalMs = 10 * 60 * 1000;
|
|
12246
12300
|
safeSetInterval("merged-pr-check", () => checkMergedPRsAndUpdateTasks(), mergedPRCheckIntervalMs);
|
|
@@ -13307,4 +13361,6 @@ export {
|
|
|
13307
13361
|
// Container runner re-exports
|
|
13308
13362
|
getContainerStatus,
|
|
13309
13363
|
isContainerEnabled,
|
|
13364
|
+
// Workflow event bridge — for fleet/kanban modules to emit events
|
|
13365
|
+
queueWorkflowEvent,
|
|
13310
13366
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
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",
|
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
"workflow-templates/planning.mjs",
|
|
263
263
|
"workflow-templates/reliability.mjs",
|
|
264
264
|
"workflow-templates/security.mjs",
|
|
265
|
+
"workflow-templates/task-batch.mjs",
|
|
265
266
|
"workflow-templates/task-lifecycle.mjs",
|
|
266
267
|
"ui/vendor/"
|
|
267
268
|
],
|