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.
- package/.env.example +9 -0
- package/agent/agent-event-bus.mjs +10 -0
- package/agent/agent-supervisor.mjs +20 -0
- package/agent/primary-agent.mjs +5 -125
- package/bosun-tui.mjs +107 -105
- package/cli.mjs +10 -0
- package/config/config.mjs +25 -0
- package/config/executor-config.mjs +124 -1
- package/infra/container-runner.mjs +565 -1
- package/infra/monitor.mjs +18 -0
- package/infra/session-tracker.mjs +112 -0
- package/infra/tracing.mjs +544 -240
- package/infra/tui-bridge.mjs +13 -1
- package/kanban/kanban-adapter.mjs +128 -4
- package/lib/repo-map.mjs +522 -0
- package/package.json +12 -4
- package/server/ui-server.mjs +302 -1
- package/task/task-archiver.mjs +18 -6
- package/task/task-attachments.mjs +14 -10
- package/task/task-cli.mjs +117 -23
- package/task/task-executor.mjs +415 -7
- package/task/task-store.mjs +381 -31
- package/telegram/telegram-bot.mjs +4 -1
- package/tui/app.mjs +131 -171
- package/tui/components/status-header.mjs +178 -75
- package/tui/lib/header-config.mjs +68 -0
- package/tui/lib/ws-bridge.mjs +61 -9
- package/tui/screens/agents.mjs +127 -0
- package/tui/screens/tasks.mjs +1 -48
- package/ui/app.js +8 -5
- package/ui/components/kanban-board.js +65 -3
- package/ui/components/session-list.js +18 -32
- package/ui/demo-defaults.js +255 -47
- package/ui/demo.html +26 -0
- package/ui/modules/session-api.js +100 -0
- package/ui/modules/state.js +71 -15
- package/ui/tabs/workflows.js +25 -1
- package/ui/tui/App.js +298 -0
- package/ui/tui/TasksScreen.js +564 -0
- package/ui/tui/constants.js +55 -0
- package/ui/tui/tasks-screen-helpers.js +301 -0
- package/ui/tui/useTasks.js +61 -0
- package/ui/tui/useWebSocket.js +166 -0
- package/ui/tui/useWorkflows.js +30 -0
- package/workflow/workflow-engine.mjs +495 -8
- package/workflow/workflow-nodes.mjs +739 -73
- package/workflow-templates/agents.mjs +3 -0
- package/workflow-templates/planning.mjs +7 -0
- package/workflow-templates/sub-workflows.mjs +15 -0
- package/workflow-templates/task-execution.mjs +33 -12
- package/workflow-templates/task-lifecycle.mjs +8 -1
- package/workspace/command-diagnostics.mjs +1 -1
- 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;
|
package/agent/primary-agent.mjs
CHANGED
|
@@ -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
|
|
21
|
+
Terminal UI for Bosun
|
|
30
22
|
|
|
31
23
|
USAGE
|
|
32
|
-
bosun
|
|
24
|
+
bosun tui [options]
|
|
25
|
+
node bosun-tui.mjs [options]
|
|
33
26
|
|
|
34
27
|
OPTIONS
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
83
|
-
const
|
|
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 (
|
|
70
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
86
71
|
showHelp();
|
|
87
|
-
|
|
72
|
+
return 0;
|
|
88
73
|
}
|
|
89
74
|
|
|
90
|
-
if (
|
|
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
|
-
|
|
78
|
+
return 0;
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|