bosun 0.41.0 → 0.41.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @module task/task-executor-pipeline
3
+ * @description Pipeline orchestration helpers extracted from task-executor.
4
+ */
5
+
6
+ import { FanoutPipeline, RacePipeline, SequentialPipeline } from "./pipeline.mjs";
7
+
8
+ function toAgentList(agents) {
9
+ if (Array.isArray(agents)) return agents.filter(Boolean);
10
+ return [agents].filter(Boolean);
11
+ }
12
+
13
+ function normalizeMode(mode) {
14
+ return String(mode || "single").trim().toLowerCase();
15
+ }
16
+
17
+ function buildStageRunner(agent, stageIndex, runner) {
18
+ const id = String(agent?.id || agent?.name || `agent-${stageIndex + 1}`);
19
+ const name = String(agent?.name || agent?.id || id);
20
+ return {
21
+ ...agent,
22
+ id,
23
+ name,
24
+ async run(input, context) {
25
+ return runner(agent, input, context);
26
+ },
27
+ };
28
+ }
29
+
30
+ export function createExecutionPipeline(mode = "single", agents = [], options = {}) {
31
+ const list = toAgentList(agents);
32
+ if (list.length === 0) {
33
+ throw new Error("At least one pipeline agent is required");
34
+ }
35
+
36
+ const runAgent =
37
+ typeof options.agentRunner === "function" ? options.agentRunner : null;
38
+ if (!runAgent) {
39
+ throw new Error("Execution pipeline requires options.agentRunner");
40
+ }
41
+
42
+ const normalizedMode = normalizeMode(mode);
43
+ const stages = list.map((agent, stageIndex) =>
44
+ buildStageRunner(agent, stageIndex, runAgent));
45
+ const pipelineOptions = {
46
+ id: options.id || `${options.task?.id || "task"}-${normalizedMode}`,
47
+ name: options.name || `${normalizedMode}-execution`,
48
+ metadata: {
49
+ taskId: options.task?.id || null,
50
+ mode: normalizedMode,
51
+ source: "task-executor",
52
+ ...(options.metadata && typeof options.metadata === "object" ? options.metadata : {}),
53
+ },
54
+ };
55
+
56
+ if (["parallel", "fanout", "parallel-slots"].includes(normalizedMode)) {
57
+ return FanoutPipeline(stages, pipelineOptions);
58
+ }
59
+ if (["failover", "race"].includes(normalizedMode)) {
60
+ return RacePipeline(stages, pipelineOptions);
61
+ }
62
+ return SequentialPipeline(stages, pipelineOptions);
63
+ }
64
+
65
+ export async function runExecutionPipeline(mode, agents, input, options = {}) {
66
+ const pipeline = createExecutionPipeline(mode, agents, options);
67
+ return pipeline.run(input, {
68
+ metadata: options.metadata || {},
69
+ signal: options.signal || null,
70
+ });
71
+ }
72
+
73
+ export async function runExecutionPipelineAgent(
74
+ agent,
75
+ input,
76
+ context,
77
+ options = {},
78
+ ) {
79
+ const runWithRetry = options.execWithRetry;
80
+ if (typeof runWithRetry !== "function") {
81
+ throw new Error("runExecutionPipelineAgent requires options.execWithRetry");
82
+ }
83
+
84
+ const prompt =
85
+ typeof agent?.prompt === "string" && agent.prompt.trim()
86
+ ? agent.prompt
87
+ : [
88
+ "Task execution pipeline",
89
+ `Mode: ${context?.options?.metadata?.mode || context?.pipelineType || "sequential"}`,
90
+ `Agent role: ${agent?.role || agent?.name || agent?.id || "agent"}`,
91
+ "Use only the structured input below; do not rely on prior conversation state.",
92
+ typeof input === "string" ? input : JSON.stringify(input, null, 2),
93
+ ]
94
+ .filter(Boolean)
95
+ .join("\n\n");
96
+
97
+ const stageIndex = Number.isInteger(context?.stageIndex)
98
+ ? context.stageIndex
99
+ : Number.isInteger(context?.agentIndex)
100
+ ? context.agentIndex
101
+ : 0;
102
+ const pipelineId = String(
103
+ context?.options?.id || context?.runId || context?.pipeline?.id || "pipeline",
104
+ );
105
+ const taskKey = `${pipelineId}-${agent?.id || agent?.name || "agent"}-${stageIndex + 1}`;
106
+ const result = await runWithRetry(prompt, {
107
+ taskKey,
108
+ cwd: options.repoRoot || process.cwd(),
109
+ timeoutMs: Number(agent?.timeoutMs || options.timeoutMs || 0) || undefined,
110
+ maxRetries: Number(agent?.maxRetries || 1) || 1,
111
+ sdk: agent?.sdk || agent?.executor || undefined,
112
+ model: agent?.model || undefined,
113
+ sessionType: "task-pipeline",
114
+ signal: context?.signal || null,
115
+ });
116
+
117
+ if (!result?.success) {
118
+ throw new Error(result?.error || `Pipeline agent ${agent?.id || "agent"} failed`);
119
+ }
120
+
121
+ return {
122
+ success: true,
123
+ output: {
124
+ text: result.output,
125
+ sdk: result.sdk,
126
+ taskKey,
127
+ },
128
+ text: result.output,
129
+ meta: {
130
+ taskKey,
131
+ sdk: result.sdk,
132
+ },
133
+ tokensUsed:
134
+ Number(result.tokensUsed || result.usage?.totalTokens || result.usage?.total_tokens || 0) ||
135
+ 0,
136
+ };
137
+ }
138
+
139
+ export default {
140
+ createExecutionPipeline,
141
+ runExecutionPipeline,
142
+ runExecutionPipelineAgent,
143
+ };
@@ -102,6 +102,11 @@ import {
102
102
  } from "./task-claims.mjs";
103
103
  import { initPresence, getPresenceState } from "../infra/presence.mjs";
104
104
  import { getSharedState } from "../workspace/shared-state-manager.mjs";
105
+ import {
106
+ createExecutionPipeline as createTaskExecutionPipeline,
107
+ runExecutionPipeline as runTaskExecutionPipeline,
108
+ runExecutionPipelineAgent,
109
+ } from "./task-executor-pipeline.mjs";
105
110
 
106
111
  // ── Constants ───────────────────────────────────────────────────────────────
107
112
 
@@ -3981,38 +3986,18 @@ class TaskExecutor {
3981
3986
  const isFreshEnough =
3982
3987
  ageMs === 0 || ageMs <= INPROGRESS_RECOVERY_MAX_AGE_MS;
3983
3988
 
3984
- // In workflow-owned mode, calling executeTask() fires task.assigned
3985
- // which launches a new workflow run. If one already exists (evidenced by
3986
- // an alive agent thread), that creates two competing runs → owner_mismatch.
3987
- // trigger.task_assigned workflows don't call action.claim_task, so ownerId
3988
- // is null and the shared-state guard above cannot protect against this.
3989
- // • Active thread → workflow is still managing it; leave it alone.
3990
- // • No active thread but fresh → agent died; reset to todo so
3991
- // trigger.task_available re-dispatches cleanly, without double-dispatch.
3989
+ // In workflow-owned mode, executor thread presence is not a reliable
3990
+ // liveness signal because workflow nodes can be actively running before
3991
+ // any executor thread key is observable. Resetting "fresh" in-progress
3992
+ // tasks here causes live runs to churn todo↔inprogress. Keep fresh tasks
3993
+ // in-progress and let stale/unstarted guards above handle true stranding.
3992
3994
  if (this.workflowOwnsTaskLifecycle) {
3993
3995
  if (hasThread) {
3994
3996
  skippedForActiveClaim++;
3995
3997
  continue;
3996
3998
  }
3997
3999
  if (isFreshEnough) {
3998
- try {
3999
- await transitionTaskStatus(id, "todo", {
4000
- source: "task-executor-recovery-workflow-owned",
4001
- });
4002
- } catch {
4003
- /* best effort */
4004
- }
4005
- try {
4006
- transitionInternalTaskStatus(
4007
- id,
4008
- "todo",
4009
- "task-executor-recovery-workflow-owned",
4010
- );
4011
- } catch {
4012
- /* best effort */
4013
- }
4014
- this._removeRuntimeSlot(id);
4015
- resetToTodo++;
4000
+ skippedForActiveClaim++;
4016
4001
  continue;
4017
4002
  }
4018
4003
  }
@@ -5041,6 +5026,30 @@ class TaskExecutor {
5041
5026
  // See workflow-templates/task-lifecycle.mjs
5042
5027
  }
5043
5028
 
5029
+ createExecutionPipeline(mode = "single", agents = [], options = {}) {
5030
+ return createTaskExecutionPipeline(mode, agents, {
5031
+ ...options,
5032
+ agentRunner:
5033
+ options.agentRunner || this._runExecutionPipelineAgent.bind(this),
5034
+ });
5035
+ }
5036
+
5037
+ async runExecutionPipeline(mode, agents, input, options = {}) {
5038
+ return await runTaskExecutionPipeline(mode, agents, input, {
5039
+ ...options,
5040
+ agentRunner:
5041
+ options.agentRunner || this._runExecutionPipelineAgent.bind(this),
5042
+ });
5043
+ }
5044
+
5045
+ async _runExecutionPipelineAgent(agent, input, context) {
5046
+ return runExecutionPipelineAgent(agent, input, context, {
5047
+ execWithRetry,
5048
+ repoRoot: this.repoRoot || process.cwd(),
5049
+ timeoutMs: this.timeoutMs || 0,
5050
+ });
5051
+ }
5052
+
5044
5053
  // ── Task Execution ────────────────────────────────────────────────────────
5045
5054
 
5046
5055
  _getWorktreeManager(repoRoot) {
@@ -5491,7 +5500,7 @@ export function loadExecutorOptionsFromConfig() {
5491
5500
  return {
5492
5501
  mode: envMode || configExec.mode || "internal",
5493
5502
  maxParallel: Number(
5494
- process.env.INTERNAL_EXECUTOR_PARALLEL || configExec.maxParallel || 3,
5503
+ process.env.INTERNAL_EXECUTOR_PARALLEL || configExec.maxParallel || 5,
5495
5504
  ),
5496
5505
  baseBranchParallelLimit: Number(
5497
5506
  process.env.INTERNAL_EXECUTOR_BASE_BRANCH_PARALLEL ||
@@ -1,71 +1,81 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const token = process.env.TELEGRAM_BOT_TOKEN;
4
-
5
- if (!token) {
6
- console.error("Missing TELEGRAM_BOT_TOKEN environment variable.");
7
- process.exit(1);
8
- }
9
-
10
- const url = `https://api.telegram.org/bot${token}/getUpdates`;
11
-
12
- async function main() {
13
- let res;
14
- try {
15
- res = await fetch(url);
16
- } catch (err) {
17
- console.error(`Fetch error: ${err.message}`);
18
- process.exit(1);
19
- }
20
-
21
- if (!res.ok) {
22
- const body = await res.text();
23
- console.error(`Request failed: ${res.status} ${body}`);
24
- process.exit(1);
25
- }
26
-
27
- const data = await res.json();
28
- if (!data.result || data.result.length === 0) {
29
- console.log(
30
- "No updates found. Send a message to the bot first, then retry.",
31
- );
32
- return;
33
- }
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
34
5
 
6
+ export function extractTelegramChatsFromUpdates(updates) {
35
7
  const chats = new Map();
36
- for (const update of data.result) {
37
- const message =
38
- update.message || update.channel_post || update.edited_message;
39
- if (!message || !message.chat) {
8
+ for (const update of Array.isArray(updates) ? updates : []) {
9
+ const message = update?.message || update?.channel_post || update?.edited_message;
10
+ const chat = message?.chat;
11
+ if (!chat?.id) {
40
12
  continue;
41
13
  }
42
- const chat = message.chat;
43
14
  if (!chats.has(chat.id)) {
44
15
  chats.set(chat.id, {
45
16
  id: chat.id,
46
- type: chat.type,
17
+ type: chat.type || "unknown",
47
18
  title: chat.title || "",
48
19
  username: chat.username || "",
49
20
  });
50
21
  }
51
22
  }
23
+ return Array.from(chats.values());
24
+ }
25
+
26
+ export async function discoverTelegramChats(token, { fetchImpl = fetch } = {}) {
27
+ const normalizedToken = String(token || "").trim();
28
+ if (!normalizedToken) {
29
+ throw new Error("Missing TELEGRAM_BOT_TOKEN environment variable.");
30
+ }
31
+
32
+ const url = `https://api.telegram.org/bot${normalizedToken}/getUpdates`;
33
+ const response = await fetchImpl(url);
34
+ if (!response.ok) {
35
+ const body = await response.text().catch(() => "");
36
+ throw new Error(`Request failed: ${response.status}${body ? ` ${body}` : ""}`);
37
+ }
52
38
 
53
- if (chats.size === 0) {
54
- console.log(
55
- "No chat IDs found in updates. Send a message to the bot first.",
56
- );
39
+ const data = await response.json();
40
+ const updates = data?.result;
41
+ const chats = extractTelegramChatsFromUpdates(updates);
42
+ if (Array.isArray(updates) && updates.length === 0) {
43
+ return {
44
+ chats,
45
+ message: "No updates found. Send a message to the bot first, then retry.",
46
+ };
47
+ }
48
+ if (chats.length === 0) {
49
+ return {
50
+ chats,
51
+ message: "No chat IDs found in updates. Send a message to the bot first.",
52
+ };
53
+ }
54
+ return { chats, message: null };
55
+ }
56
+
57
+ async function main() {
58
+ const token = process.env.TELEGRAM_BOT_TOKEN;
59
+ const { chats, message } = await discoverTelegramChats(token);
60
+ if (message) {
61
+ console.log(message);
57
62
  return;
58
63
  }
59
64
 
60
65
  console.log("Found chat IDs:");
61
- for (const chat of chats.values()) {
62
- const titlePart = chat.title ? ` title=\"${chat.title}\"` : "";
66
+ for (const chat of chats) {
67
+ const titlePart = chat.title ? ` title="${chat.title}"` : "";
63
68
  const userPart = chat.username ? ` username=@${chat.username}` : "";
64
69
  console.log(`- id=${chat.id} type=${chat.type}${userPart}${titlePart}`);
65
70
  }
66
71
  }
67
72
 
68
- main().catch((err) => {
69
- console.error(`Error: ${err.message || err}`);
70
- process.exit(1);
71
- });
73
+ const __filename = fileURLToPath(import.meta.url);
74
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(__filename)) {
75
+ try {
76
+ await main();
77
+ } catch (err) {
78
+ console.error(`Error: ${err.message || err}`);
79
+ process.exit(1);
80
+ }
81
+ }
@@ -470,7 +470,7 @@ function ExecutorConfigPanel({ ws }) {
470
470
 
471
471
  <${Collapse} in=${expanded}>
472
472
  <${Stack} spacing=${1.5} sx=${{ pt: 1, pb: 0.5, px: 0.5 }}>
473
- <!-- Max concurrent executors -->
473
+
474
474
  <${Box}>
475
475
  <${Typography} variant="caption" color="text.secondary" sx=${{ mb: 0.5, display: "block" }}>
476
476
  Max Concurrent Executors: ${maxConcurrent}
@@ -492,7 +492,7 @@ function ExecutorConfigPanel({ ws }) {
492
492
  />
493
493
  <//>
494
494
 
495
- <!-- Pool mode -->
495
+
496
496
  <${Stack} direction="row" spacing=${1} alignItems="center">
497
497
  <${Typography} variant="caption" color="text.secondary">Pool:<//>
498
498
  <${ToggleButtonGroup}
@@ -515,7 +515,7 @@ function ExecutorConfigPanel({ ws }) {
515
515
  <//>
516
516
  <//>
517
517
 
518
- <!-- Weight (only for shared pool) -->
518
+
519
519
  ${pool === "shared" && html`
520
520
  <${Box}>
521
521
  <${Typography} variant="caption" color="text.secondary" sx=${{ mb: 0.5, display: "block" }}>
@@ -533,7 +533,7 @@ function ExecutorConfigPanel({ ws }) {
533
533
  <//>
534
534
  `}
535
535
 
536
- <!-- Save button -->
536
+
537
537
  ${hasChanges && html`
538
538
  <${Button}
539
539
  size="small"
@@ -630,12 +630,12 @@ function WorkspaceCard({ ws }) {
630
630
  <//>
631
631
  <//>
632
632
 
633
- <!-- State toggle row -->
633
+
634
634
  <${Box} sx=${{ mt: 1.5, mb: 0.5 }}>
635
635
  <${WorkspaceStateToggle} ws=${ws} />
636
636
  <//>
637
637
 
638
- <!-- Executor config -->
638
+
639
639
  <${ExecutorConfigPanel} ws=${ws} />
640
640
  <//>
641
641
 
@@ -780,7 +780,7 @@ export function WorkspaceManager({ open, onClose }) {
780
780
 
781
781
  return html`
782
782
  <${Modal} title="Manage Workspaces" open=${open} onClose=${onClose}>
783
- <!-- State summary bar -->
783
+
784
784
  ${wsList.length > 0 && html`
785
785
  <${Stack} direction="row" spacing=${1.5} sx=${{ mb: 2 }} alignItems="center">
786
786
  ${Object.entries(STATE_CONFIG).map(([key, cfg]) => html`