bosun 0.42.1 → 0.42.4

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 (53) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/agent/primary-agent.mjs +5 -125
  5. package/bosun-tui.mjs +107 -105
  6. package/cli.mjs +10 -0
  7. package/config/config.mjs +25 -0
  8. package/config/executor-config.mjs +124 -1
  9. package/infra/container-runner.mjs +565 -1
  10. package/infra/monitor.mjs +18 -0
  11. package/infra/session-tracker.mjs +112 -0
  12. package/infra/tracing.mjs +544 -240
  13. package/infra/tui-bridge.mjs +13 -1
  14. package/kanban/kanban-adapter.mjs +128 -4
  15. package/lib/repo-map.mjs +522 -0
  16. package/package.json +12 -4
  17. package/server/ui-server.mjs +302 -1
  18. package/task/task-archiver.mjs +18 -6
  19. package/task/task-attachments.mjs +14 -10
  20. package/task/task-cli.mjs +117 -23
  21. package/task/task-executor.mjs +415 -7
  22. package/task/task-store.mjs +381 -31
  23. package/telegram/telegram-bot.mjs +4 -1
  24. package/tui/app.mjs +131 -171
  25. package/tui/components/status-header.mjs +178 -75
  26. package/tui/lib/header-config.mjs +68 -0
  27. package/tui/lib/ws-bridge.mjs +61 -9
  28. package/tui/screens/agents.mjs +127 -0
  29. package/tui/screens/tasks.mjs +1 -48
  30. package/ui/app.js +8 -5
  31. package/ui/components/kanban-board.js +65 -3
  32. package/ui/components/session-list.js +18 -32
  33. package/ui/demo-defaults.js +255 -47
  34. package/ui/demo.html +26 -0
  35. package/ui/modules/session-api.js +100 -0
  36. package/ui/modules/state.js +71 -15
  37. package/ui/tabs/workflows.js +25 -1
  38. package/ui/tui/App.js +298 -0
  39. package/ui/tui/TasksScreen.js +564 -0
  40. package/ui/tui/constants.js +55 -0
  41. package/ui/tui/tasks-screen-helpers.js +301 -0
  42. package/ui/tui/useTasks.js +61 -0
  43. package/ui/tui/useWebSocket.js +166 -0
  44. package/ui/tui/useWorkflows.js +30 -0
  45. package/workflow/workflow-engine.mjs +495 -8
  46. package/workflow/workflow-nodes.mjs +739 -73
  47. package/workflow-templates/agents.mjs +3 -0
  48. package/workflow-templates/planning.mjs +7 -0
  49. package/workflow-templates/sub-workflows.mjs +15 -0
  50. package/workflow-templates/task-execution.mjs +33 -12
  51. package/workflow-templates/task-lifecycle.mjs +8 -1
  52. package/workspace/command-diagnostics.mjs +1 -1
  53. package/workspace/context-cache.mjs +182 -9
package/.env.example CHANGED
@@ -802,6 +802,11 @@ VK_RECOVERY_PORT=54089
802
802
  # BOSUN_HOOKS_DISABLE_TASK_COMPLETE=false
803
803
  # BOSUN_HOOKS_DISABLE_HEALTH_CHECK=false
804
804
 
805
+ # ── OpenTelemetry Tracing & Metrics ───────────────────────────────────────────
806
+ # External orchestration-layer observability only; never affects agent context.
807
+ # BOSUN_OTEL_ENDPOINT=http://localhost:4318/v1/traces
808
+ # Configure tracing.sampleRate in bosun.config.json to tune sampling.
809
+
805
810
  # Force hooks to fire even for non-managed sessions (debug only):
806
811
  # BOSUN_HOOKS_FORCE=false
807
812
 
@@ -1175,3 +1180,7 @@ COPILOT_CLOUD_DISABLED=true
1175
1180
  # AGENT_STUCK_THRESHOLD_MS=300000
1176
1181
  # Alert if session costs more than $N (default: 1.0)
1177
1182
  # AGENT_COST_ANOMALY_THRESHOLD=1.0
1183
+
1184
+ # OpenTelemetry tracing (optional)
1185
+ # BOSUN_OTEL_ENDPOINT=http://localhost:4318/v1/traces
1186
+
@@ -25,6 +25,7 @@ import {
25
25
  reduceRetryQueue,
26
26
  snapshotRetryQueue,
27
27
  } from "./retry-queue.mjs";
28
+ import { addSpanEvent, recordAgentError, recordIntervention } from "../infra/tracing.mjs";
28
29
 
29
30
  const TAG = "[agent-event-bus]";
30
31
 
@@ -232,6 +233,14 @@ export class AgentEventBus {
232
233
  emit(type, taskId, payload = {}, opts = {}) {
233
234
  const ts = Date.now();
234
235
  const event = { type, taskId, payload, ts };
236
+ addSpanEvent(type, { "bosun.task.id": taskId, ...payload });
237
+ if (type === AGENT_EVENT.AGENT_ERROR) {
238
+ recordAgentError(payload?.errorType || payload?.classification || "agent_error", {
239
+ "bosun.task.id": taskId,
240
+ "bosun.executor": payload?.executor,
241
+ "bosun.agent.sdk": payload?.sdk,
242
+ });
243
+ }
235
244
 
236
245
  // ── Dedup
237
246
  const key = `${type}:${taskId}`;
@@ -1092,3 +1101,4 @@ export function createAgentEventBus(options) {
1092
1101
  return new AgentEventBus(options);
1093
1102
  }
1094
1103
 
1104
+
@@ -29,6 +29,8 @@
29
29
  * @module agent-supervisor
30
30
  */
31
31
 
32
+ import { addSpanEvent, recordIntervention } from "../infra/tracing.mjs";
33
+
32
34
  const TAG = "[agent-supervisor]";
33
35
  const API_ERROR_CONTINUE_COOLDOWNS_MS = Object.freeze([
34
36
  3 * 60_000,
@@ -473,6 +475,12 @@ export class AgentSupervisor {
473
475
  }
474
476
 
475
477
  this._lastDecision.set(taskId, { situation, intervention, ts: Date.now() });
478
+ addSpanEvent("bosun.supervisor.assess", {
479
+ "bosun.task.id": taskId,
480
+ "bosun.health.score": healthScore,
481
+ "bosun.situation": situation,
482
+ "bosun.intervention.type": intervention,
483
+ });
476
484
 
477
485
  return { situation, healthScore, intervention, prompt, reason };
478
486
  }
@@ -489,6 +497,18 @@ export class AgentSupervisor {
489
497
  );
490
498
 
491
499
  try {
500
+ if (intervention !== INTERVENTION.NONE) {
501
+ recordIntervention(intervention, {
502
+ "bosun.task.id": taskId,
503
+ "bosun.situation": situation,
504
+ });
505
+ }
506
+ addSpanEvent("bosun.supervisor.intervention", {
507
+ "bosun.task.id": taskId,
508
+ "bosun.intervention.type": intervention,
509
+ "bosun.situation": situation,
510
+ "bosun.reason": reason,
511
+ });
492
512
  switch (intervention) {
493
513
  case INTERVENTION.NONE:
494
514
  break;
@@ -9,6 +9,7 @@ import { loadConfig } from "../config/config.mjs";
9
9
  import { ensureCodexConfig, printConfigSummary } from "../shell/codex-config.mjs";
10
10
  import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config.mjs";
11
11
  import { resolveRepoRoot } from "../config/repo-root.mjs";
12
+ import { buildArchitectEditorFrame } from "../lib/repo-map.mjs";
12
13
  import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
13
14
  import { getSessionTracker } from "../infra/session-tracker.mjs";
14
15
  import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
@@ -191,133 +192,9 @@ function appendAttachmentsToPrompt(message, attachments) {
191
192
  return { message: `${message}${lines.join("\n")}`, appended: true };
192
193
  }
193
194
 
194
- function normalizeRepoMap(repoMap) {
195
- if (!repoMap || typeof repoMap !== "object") return null;
196
- const root = String(repoMap.root || repoMap.repoRoot || "").trim();
197
- const files = Array.isArray(repoMap.files)
198
- ? repoMap.files
199
- .filter((entry) => entry && typeof entry === "object")
200
- .map((entry) => ({
201
- path: String(entry.path || entry.file || "").trim(),
202
- summary: String(entry.summary || entry.description || "").trim(),
203
- symbols: Array.isArray(entry.symbols)
204
- ? entry.symbols.map((symbol) => String(symbol || "").trim()).filter(Boolean)
205
- : [],
206
- }))
207
- .filter((entry) => entry.path)
208
- : [];
209
- if (!root && files.length === 0) return null;
210
- return { root, files };
211
- }
212
-
213
- function formatRepoMap(repoMap) {
214
- const normalized = normalizeRepoMap(repoMap);
215
- if (!normalized) return "";
216
- const lines = ["## Repo Map"];
217
- if (normalized.root) lines.push(`- Root: ${normalized.root}`);
218
- for (const file of normalized.files) {
219
- const parts = [file.path];
220
- if (file.symbols.length) parts.push(`symbols: ${file.symbols.join(", ")}`);
221
- if (file.summary) parts.push(file.summary);
222
- lines.push(`- ${parts.join(" — ")}`);
223
- }
224
- return lines.join("\n");
225
- }
226
-
227
- function summarizePathSegment(segment) {
228
- return String(segment || "")
229
- .replace(/[-_]+/g, " ")
230
- .replace(/\.m?js$/i, "")
231
- .replace(/\s+/g, " ")
232
- .trim();
233
- }
234
-
235
- function inferRepoMapEntry(pathValue) {
236
- const path = String(pathValue || "").trim().replace(/\\/g, "/");
237
- if (!path) return null;
238
- const name = path.split("/").pop() || path;
239
- const stem = summarizePathSegment(name);
240
- const dir = path.includes("/") ? path.split("/").slice(0, -1).join("/") : "";
241
- const dirHint = dir ? summarizePathSegment(dir.split("/").pop()) : "";
242
- const symbols = [];
243
- const lowerStem = stem.toLowerCase();
244
- if (lowerStem) {
245
- const compact = lowerStem
246
- .split(" ")
247
- .filter(Boolean)
248
- .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
249
- .join("");
250
- if (compact) {
251
- symbols.push(compact);
252
- if (!compact.startsWith("test")) symbols.push(`test${compact.charAt(0).toUpperCase()}${compact.slice(1)}`);
253
- }
254
- }
255
- const summaryParts = [];
256
- if (dirHint) summaryParts.push(`${dirHint} module`);
257
- if (stem) summaryParts.push(stem);
258
- return {
259
- path,
260
- summary: summaryParts.join(" — "),
261
- symbols: [...new Set(symbols)].slice(0, 3),
262
- };
263
- }
264
195
 
265
- function deriveRepoMap(options = {}) {
266
- const explicit = normalizeRepoMap(options.repoMap);
267
- if (explicit) return explicit;
268
- const changedFiles = Array.isArray(options.changedFiles)
269
- ? options.changedFiles.map((value) => String(value || "").trim()).filter(Boolean)
270
- : [];
271
- if (!changedFiles.length) return null;
272
- const root = String(options.repoRoot || options.cwd || resolveRepoRoot() || "").trim();
273
- const files = changedFiles
274
- .map((pathValue) => inferRepoMapEntry(pathValue))
275
- .filter(Boolean)
276
- .slice(0, Number(options.repoMapFileLimit) > 0 ? Number(options.repoMapFileLimit) : 12);
277
- if (!root && files.length === 0) return null;
278
- return { root, files };
279
- }
280
196
 
281
- function inferExecutionRole(options = {}, effectiveMode = "agent") {
282
- const explicitRole = String(options.executionRole || "").trim().toLowerCase();
283
- if (explicitRole) return explicitRole;
284
- if (effectiveMode === "plan") return "architect";
285
- const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
286
- if (architectPlan) return "editor";
287
- return "";
288
- }
289
- function buildArchitectEditorFrame(options = {}, effectiveMode = "agent") {
290
- const executionRole = inferExecutionRole(options, effectiveMode);
291
- const repoMapBlock = formatRepoMap(deriveRepoMap(options));
292
- const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
293
- const lines = ["## Architect/Editor Execution"];
294
-
295
- if (executionRole === "architect") {
296
- lines.push(
297
- "You are the architect phase.",
298
- "Do not implement code changes in this phase.",
299
- "Use the repo map to produce a compact structural plan that an editor can execute and validate.",
300
- "Editor handoff: include ordered implementation steps, touched files, risks, and validation guidance.",
301
- );
302
- } else if (executionRole === "editor") {
303
- lines.push(
304
- "You are the editor phase.",
305
- "Implement the approved plan with focused edits and verification.",
306
- "Prefer the supplied repo map over broad rediscovery unless validation reveals drift.",
307
- );
308
- if (architectPlan) {
309
- lines.push("", "## Architect Plan", architectPlan);
310
- }
311
- } else {
312
- return repoMapBlock;
313
- }
314
-
315
- if (repoMapBlock) {
316
- lines.push("", repoMapBlock);
317
- }
318
197
 
319
- return lines.join("\n");
320
- }
321
198
 
322
199
  function summarizeContextCompressionItems(items) {
323
200
  if (!Array.isArray(items) || items.length === 0) return null;
@@ -1190,8 +1067,9 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
1190
1067
  const messageWithAttachments = attachments.length && !attachmentsAppended
1191
1068
  ? appendAttachmentsToPrompt(userMessage, attachments).message
1192
1069
  : userMessage;
1070
+ const architectEditorFrame = buildArchitectEditorFrame(options, effectiveMode);
1193
1071
  const toolContract = buildPrimaryToolCapabilityContract(options);
1194
- const messageWithToolContract = [selectedProfile.block, toolContract, messageWithAttachments]
1072
+ const messageWithToolContract = [selectedProfile.block, architectEditorFrame, toolContract, messageWithAttachments]
1195
1073
  .filter(Boolean)
1196
1074
  .join("\n\n");
1197
1075
  const framedMessage = modePrefix ? modePrefix + messageWithToolContract : messageWithToolContract;
@@ -1680,3 +1558,5 @@ export async function execSdkCommand(command, args = "", adapterName, options =
1680
1558
  }
1681
1559
 
1682
1560
 
1561
+
1562
+
package/bosun-tui.mjs CHANGED
@@ -1,142 +1,144 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * bosun-tui — Terminal User Interface for Bosun
5
- *
6
- * A terminal-based UI for monitoring Bosun agents, tasks, and workflows.
7
- * Built with Ink (React-like CLI framework).
8
- *
9
- * Usage:
10
- * bosun-tui # Start the TUI
11
- * bosun-tui --help # Show help
12
- * bosun-tui --port 3080 # Connect to specific port
13
- */
14
-
15
- import { resolve, dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
3
  import { readFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import loadConfig from "./config/config.mjs";
8
+ import { resolveWebSocketProtocol } from "./tui/lib/ws-bridge.mjs";
9
+
10
+ const MIN_COLUMNS = 120;
11
+ const MIN_ROWS = 30;
12
+
18
13
 
19
14
  const __filename = fileURLToPath(import.meta.url);
20
15
  const __dirname = dirname(__filename);
21
16
 
22
17
  function showHelp() {
23
- const version = JSON.parse(
24
- readFileSync(resolve(__dirname, "package.json"), "utf8"),
25
- ).version;
26
-
18
+ const version = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8")).version;
27
19
  console.log(`
28
20
  bosun-tui v${version}
29
- Terminal User Interface for Bosun
21
+ Terminal UI for Bosun
30
22
 
31
23
  USAGE
32
- bosun-tui [options]
24
+ bosun tui [options]
25
+ node bosun-tui.mjs [options]
33
26
 
34
27
  OPTIONS
35
- --port <n> UI server port to connect (default: 3080 or TELEGRAM_UI_PORT env)
36
- --host <host> UI server host (default: localhost)
37
- --connect Connect to existing UI server (don't start monitor)
38
- --screen <name> Initial screen (tasks|agents|status)
39
- --refresh <ms> Stats refresh interval (default: 2000ms)
40
- --help Show this help
41
- --version Show version
42
-
43
- SCREENS
44
- tasks Kanban board with task CRUD
45
- agents Live agent session table
46
- status System status overview
47
-
48
- KEYBOARD NAVIGATION
49
- Tab / Shift+Tab Navigate between panels
50
- ↑↓←→ Navigate within panels
51
- Enter Select / Execute action
52
- Esc Back / Close modal
53
- c Create new task (tasks screen)
54
- r Resume selected agent session (Agents screen)
55
- q Quit
56
-
57
- EXAMPLES
58
- bosun-tui --port 3080
59
- bosun-tui --screen tasks
60
- bosun-tui --connect --port 3080
28
+ --host <host> WebSocket host (default: 127.0.0.1)
29
+ --port <n> WebSocket/UI port (default: TELEGRAM_UI_PORT or 3080)
30
+ --screen <name> Initial screen (agents|tasks|logs|workflows|telemetry|settings|help)
31
+ --help Show this help
32
+ --version Show version
61
33
  `);
62
34
  }
63
35
 
64
- function getArgValue(flag, defaultValue = "") {
65
- const args = process.argv.slice(2);
66
- const match = args.find((arg) => arg.startsWith(`${flag}=`));
67
- if (match) {
68
- return match.slice(flag.length + 1).trim();
69
- }
70
- const idx = args.indexOf(flag);
71
- if (idx >= 0 && args[idx + 1] && !args[idx + 1].startsWith("--")) {
72
- return args[idx + 1].trim();
36
+ function getArgValue(args, flag, defaultValue = "") {
37
+ const inline = args.find((arg) => arg.startsWith(`${flag}=`));
38
+ if (inline) return inline.slice(flag.length + 1).trim();
39
+ const index = args.indexOf(flag);
40
+ if (index >= 0 && args[index + 1] && !args[index + 1].startsWith("--")) {
41
+ return args[index + 1].trim();
73
42
  }
74
43
  return defaultValue;
75
44
  }
76
45
 
77
- function getArgFlag(flag) {
78
- const args = process.argv.slice(2);
79
- return args.includes(flag);
46
+ function hasFlag(args, ...flags) {
47
+ return flags.some((flag) => args.includes(flag));
48
+ }
49
+
50
+ function getTerminalSize(stdout = process.stdout) {
51
+ return {
52
+ columns: Math.max(0, Number(stdout?.columns || 0)),
53
+ rows: Math.max(0, Number(stdout?.rows || 0)),
54
+ };
55
+ }
56
+
57
+ function resolvePort(config) {
58
+ return Number(process.env.TELEGRAM_UI_PORT || process.env.BOSUN_PORT || config?.telegramUiPort || "3080") || 3080;
59
+ }
60
+
61
+ function renderApp(instance, React, App, props) {
62
+ instance.rerender(React.createElement(App, props));
80
63
  }
81
64
 
82
- async function main() {
83
- const args = process.argv.slice(2);
65
+ export async function runBosunTui(argv = process.argv.slice(2), options = {}) {
66
+ const stdout = options.stdout || process.stdout;
67
+ const stderr = options.stderr || process.stderr;
68
+ const args = Array.isArray(argv) ? argv : [];
84
69
 
85
- if (getArgFlag("--help") || args.includes("-h")) {
70
+ if (hasFlag(args, "--help", "-h")) {
86
71
  showHelp();
87
- process.exit(0);
72
+ return 0;
88
73
  }
89
74
 
90
- if (getArgFlag("--version") || args.includes("-v")) {
91
- const version = JSON.parse(
92
- readFileSync(resolve(__dirname, "package.json"), "utf8"),
93
- ).version;
75
+ if (hasFlag(args, "--version", "-v")) {
76
+ const version = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8")).version;
94
77
  console.log(`bosun-tui v${version}`);
95
- process.exit(0);
78
+ return 0;
96
79
  }
97
80
 
98
- const port = Number(getArgValue("--port", process.env.TELEGRAM_UI_PORT || "3080")) || 3080;
99
- const host = getArgValue("--host", "localhost");
100
- const connectOnly = getArgFlag("--connect");
101
- const initialScreen = getArgValue("--screen", "status");
102
- const refreshMs = Number(getArgValue("--refresh", "2000")) || 2000;
81
+ if (!stdout?.isTTY) {
82
+ stderr.write("[bosun-tui] Error: stdout is not a TTY. Run `bosun tui` in an interactive terminal.\n");
83
+ return 1;
84
+ }
103
85
 
104
- console.log(`[bosun-tui] Starting...`);
105
- console.log(`[bosun-tui] Connecting to ${host}:${port}`);
86
+ globalThis.WebSocket = globalThis.WebSocket || (await import("ws")).WebSocket;
87
+
88
+ const config = loadConfig([process.argv[0], __filename, ...args]);
89
+ const configDir = String(config?.configDir || process.env.BOSUN_DIR || resolve(process.cwd(), ".bosun")).trim();
90
+ const host = getArgValue(args, "--host", "127.0.0.1");
91
+ const port = Number(getArgValue(args, "--port", String(resolvePort(config)))) || resolvePort(config);
92
+ const protocol = getArgValue(
93
+ args,
94
+ "--protocol",
95
+ resolveWebSocketProtocol({ configDir }),
96
+ );
97
+ const initialScreen = getArgValue(args, "--screen", "agents");
98
+
99
+ const React = await import("react");
100
+ const ink = await import("ink");
101
+ const { default: App } = await import("./ui/tui/App.js");
102
+
103
+ let terminalSize = getTerminalSize(stdout);
104
+ const props = {
105
+ config,
106
+ configDir,
107
+ host,
108
+ port,
109
+ protocol,
110
+ initialScreen,
111
+ terminalSize,
112
+ };
113
+
114
+ const instance = ink.render(React.createElement(App, props), { exitOnCtrlC: true });
115
+
116
+ const onResize = () => {
117
+ terminalSize = getTerminalSize(stdout);
118
+ renderApp(instance, React, App, { ...props, terminalSize });
119
+ };
120
+
121
+ stdout.on?.("resize", onResize);
106
122
 
107
123
  try {
108
- const { render } = await import("ink");
109
- const importErrors = [];
110
-
111
- let App;
112
- try {
113
- const appModule = await import("./tui/app.mjs");
114
- App = appModule.default;
115
- } catch (importErr) {
116
- importErrors.push(`App: ${importErr.message}`);
117
- console.error(`[bosun-tui] Failed to import TUI app: ${importErr.message}`);
118
- console.log(`[bosun-tui] TUI requires ink. Install with: npm install ink`);
119
- process.exit(1);
124
+ if (typeof instance.waitUntilExit === "function") {
125
+ await instance.waitUntilExit();
120
126
  }
121
-
122
- const { waitUntilExit } = await import("ink");
123
-
124
- const app = render(
125
- App({
126
- host,
127
- port,
128
- connectOnly,
129
- initialScreen,
130
- refreshMs,
131
- }),
132
- );
133
-
134
- process.exitCode = await waitUntilExit(app);
135
- } catch (err) {
136
- console.error(`[bosun-tui] Failed to start: ${err.message}`);
137
- console.log(`[bosun-tui] Ensure bosun is running or use --connect to connect to an existing UI server`);
138
- process.exit(1);
127
+ return 0;
128
+ } finally {
129
+ stdout.off?.("resize", onResize);
139
130
  }
140
131
  }
141
132
 
142
- main();
133
+ if (process.argv[1] && resolve(process.argv[1]) === __filename) {
134
+ runBosunTui(process.argv.slice(2))
135
+ .then((code) => {
136
+ process.exit(code ?? 0);
137
+ })
138
+ .catch((error) => {
139
+ console.error(`[bosun-tui] Failed to start: ${error?.message || error}`);
140
+ process.exit(1);
141
+ });
142
+ }
143
+
144
+
package/cli.mjs CHANGED
@@ -79,6 +79,7 @@ function showHelp() {
79
79
  COMMANDS
80
80
  workflow list List declarative pipeline workflows
81
81
  workflow run <name> Run a declarative pipeline workflow
82
+ tui Launch the terminal UI
82
83
  audit <command> Run codebase annotation audit tools (scan|generate|warn|manifest|index|trim|conformity|migrate)
83
84
  --setup Launch the web-based setup wizard (default)
84
85
  --setup-terminal Run the legacy terminal setup wizard
@@ -167,6 +168,7 @@ function showHelp() {
167
168
  workflow run <name> Run a declarative fresh-context workflow
168
169
 
169
170
  Run 'bosun workflow --help' for workflow CLI examples.
171
+ Run 'bosun tui' to launch the terminal UI.
170
172
 
171
173
  VIBE-KANBAN
172
174
  --no-vk-spawn Don't auto-spawn Vibe-Kanban
@@ -1425,6 +1427,12 @@ async function main() {
1425
1427
  process.exit(0);
1426
1428
  }
1427
1429
 
1430
+ if (args[0] === "tui") {
1431
+ const { runBosunTui } = await import("./bosun-tui.mjs");
1432
+ const exitCode = await runBosunTui(args.slice(1));
1433
+ process.exit(exitCode ?? 0);
1434
+ }
1435
+
1428
1436
  // Handle --help
1429
1437
  if (args.includes("--help") || args.includes("-h")) {
1430
1438
  showHelp();
@@ -2710,3 +2718,5 @@ main().catch(async (err) => {
2710
2718
  await sendCrashNotification(1, null).catch(() => {});
2711
2719
  process.exit(1);
2712
2720
  });
2721
+
2722
+
package/config/config.mjs CHANGED
@@ -1811,6 +1811,10 @@ export function loadConfig(argv = process.argv, options = {}) {
1811
1811
  `http://127.0.0.1:${vkRecoveryPort}`;
1812
1812
  const vkPublicUrl = process.env.VK_PUBLIC_URL || process.env.VK_WEB_URL || "";
1813
1813
  const vkTaskUrlTemplate = process.env.VK_TASK_URL_TEMPLATE || "";
1814
+ const tracingEndpoint =
1815
+ process.env.BOSUN_OTEL_ENDPOINT || configData?.tracing?.endpoint || null;
1816
+ const tracingEnabled = configData?.tracing?.enabled ?? Boolean(tracingEndpoint);
1817
+ const tracingSampleRate = Number(configData?.tracing?.sampleRate ?? 1);
1814
1818
  const vkRecoveryCooldownMin = Number(
1815
1819
  process.env.VK_RECOVERY_COOLDOWN_MIN || "10",
1816
1820
  );
@@ -2127,6 +2131,20 @@ export function loadConfig(argv = process.argv, options = {}) {
2127
2131
  // Voice assistant
2128
2132
  voice: Object.freeze(configData.voice || {}),
2129
2133
 
2134
+ // OpenTelemetry tracing
2135
+ tracing: Object.freeze({
2136
+ enabled:
2137
+ typeof configData.tracing?.enabled === "boolean"
2138
+ ? configData.tracing.enabled
2139
+ : Boolean(configData.tracing?.endpoint || process.env.BOSUN_OTEL_ENDPOINT || ""),
2140
+ endpoint:
2141
+ configData.tracing?.endpoint || process.env.BOSUN_OTEL_ENDPOINT || "",
2142
+ sampleRate:
2143
+ Number.isFinite(Number(configData.tracing?.sampleRate))
2144
+ ? Number(configData.tracing.sampleRate)
2145
+ : 1.0,
2146
+ }),
2147
+
2130
2148
  // Merge Strategy
2131
2149
  codexAnalyzeMergeStrategy:
2132
2150
  codexEnabled &&
@@ -2144,6 +2162,11 @@ export function loadConfig(argv = process.argv, options = {}) {
2144
2162
  vkEndpointUrl,
2145
2163
  vkPublicUrl,
2146
2164
  vkTaskUrlTemplate,
2165
+ tracing: {
2166
+ enabled: tracingEnabled,
2167
+ endpoint: tracingEndpoint,
2168
+ sampleRate: Number.isFinite(tracingSampleRate) ? tracingSampleRate : 1,
2169
+ },
2147
2170
  vkRecoveryCooldownMin,
2148
2171
  vkRuntimeRequired,
2149
2172
  vkSpawnEnabled,
@@ -2318,3 +2341,5 @@ export {
2318
2341
  resolveAgentRepoRoot,
2319
2342
  };
2320
2343
  export default loadConfig;
2344
+
2345
+