bosun 0.42.2 → 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/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/tracing.mjs +544 -240
- package/infra/tui-bridge.mjs +13 -1
- package/kanban/kanban-adapter.mjs +128 -4
- package/lib/repo-map.mjs +114 -3
- package/package.json +11 -4
- package/server/ui-server.mjs +3 -0
- package/task/task-archiver.mjs +18 -6
- package/task/task-attachments.mjs +14 -10
- package/task/task-cli.mjs +24 -4
- package/task/task-executor.mjs +19 -0
- package/task/task-store.mjs +194 -37
- 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 +52 -2
- 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 +412 -7
- package/workflow/workflow-nodes.mjs +616 -75
- package/workflow-templates/agents.mjs +3 -0
- package/workflow-templates/planning.mjs +7 -0
- package/workflow-templates/sub-workflows.mjs +5 -0
- package/workflow-templates/task-execution.mjs +3 -0
- 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/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
|
+
|
|
@@ -122,6 +122,84 @@ const DEFAULT_EXECUTORS = {
|
|
|
122
122
|
distribution: "primary-only",
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
const DEFAULT_WORKFLOW_OFFLOAD_POLICY = Object.freeze({
|
|
126
|
+
enabled: true,
|
|
127
|
+
nodeTypes: ["validation.tests", "validation.build", "validation.lint"],
|
|
128
|
+
commandTypes: ["test", "build", "qualityGate"],
|
|
129
|
+
commandPatterns: [
|
|
130
|
+
"\\b(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?test\\b",
|
|
131
|
+
"\\b(?:npm|pnpm|yarn|bun)\\s+run\\s+build\\b",
|
|
132
|
+
"\\b(?:npm|pnpm|yarn|bun)\\s+run\\s+lint\\b",
|
|
133
|
+
"\\bvitest\\b",
|
|
134
|
+
"\\bjest\\b",
|
|
135
|
+
"\\btsc\\b",
|
|
136
|
+
"\\bpre-?push\\b",
|
|
137
|
+
"\\bgit\\s+diff\\b",
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
function parseBooleanFlag(value, fallback = false) {
|
|
142
|
+
if (value == null || value === "") return fallback;
|
|
143
|
+
if (typeof value === "boolean") return value;
|
|
144
|
+
if (typeof value === "number") return value !== 0;
|
|
145
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
146
|
+
if (!normalized) return fallback;
|
|
147
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
148
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeWorkflowOffloadPolicy(configData = {}) {
|
|
153
|
+
const rawPolicy =
|
|
154
|
+
configData?.workflowOffloadPolicy && typeof configData.workflowOffloadPolicy === "object"
|
|
155
|
+
? configData.workflowOffloadPolicy
|
|
156
|
+
: configData?.executors?.workflowOffloadPolicy &&
|
|
157
|
+
typeof configData.executors.workflowOffloadPolicy === "object"
|
|
158
|
+
? configData.executors.workflowOffloadPolicy
|
|
159
|
+
: {};
|
|
160
|
+
|
|
161
|
+
const parsePatterns = (value) =>
|
|
162
|
+
parseListValue(value)
|
|
163
|
+
.map((entry) => {
|
|
164
|
+
try {
|
|
165
|
+
return new RegExp(entry, "i");
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
|
|
172
|
+
const nodeTypes = parseListValue(
|
|
173
|
+
process.env.HEAVY_RUNNER_OFFLOAD_NODE_TYPES ??
|
|
174
|
+
rawPolicy.nodeTypes ??
|
|
175
|
+
DEFAULT_WORKFLOW_OFFLOAD_POLICY.nodeTypes,
|
|
176
|
+
);
|
|
177
|
+
const commandTypes = parseListValue(
|
|
178
|
+
process.env.HEAVY_RUNNER_OFFLOAD_COMMAND_TYPES ??
|
|
179
|
+
rawPolicy.commandTypes ??
|
|
180
|
+
DEFAULT_WORKFLOW_OFFLOAD_POLICY.commandTypes,
|
|
181
|
+
);
|
|
182
|
+
const commandPatterns = parsePatterns(
|
|
183
|
+
process.env.HEAVY_RUNNER_OFFLOAD_COMMAND_PATTERNS ??
|
|
184
|
+
rawPolicy.commandPatterns ??
|
|
185
|
+
DEFAULT_WORKFLOW_OFFLOAD_POLICY.commandPatterns,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
enabled: parseBooleanFlag(
|
|
190
|
+
process.env.HEAVY_RUNNER_OFFLOAD_ENABLED ?? rawPolicy.enabled,
|
|
191
|
+
DEFAULT_WORKFLOW_OFFLOAD_POLICY.enabled,
|
|
192
|
+
),
|
|
193
|
+
nodeTypes: new Set(
|
|
194
|
+
nodeTypes.map((entry) => String(entry || "").trim()).filter(Boolean),
|
|
195
|
+
),
|
|
196
|
+
commandTypes: new Set(
|
|
197
|
+
commandTypes.map((entry) => String(entry || "").trim()).filter(Boolean),
|
|
198
|
+
),
|
|
199
|
+
commandPatterns,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
125
203
|
function parseExecutorsFromEnv() {
|
|
126
204
|
// EXECUTORS=CODEX:DEFAULT:100:gpt-5.2-codex|gpt-5.1-codex-mini
|
|
127
205
|
const raw = process.env.EXECUTORS;
|
|
@@ -256,7 +334,12 @@ export function loadExecutorConfig(configDir, configData) {
|
|
|
256
334
|
process.env.EXECUTOR_DISTRIBUTION ||
|
|
257
335
|
DEFAULT_EXECUTORS.distribution;
|
|
258
336
|
|
|
259
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
executors,
|
|
339
|
+
failover,
|
|
340
|
+
distribution,
|
|
341
|
+
workflowOffloadPolicy: normalizeWorkflowOffloadPolicy(configData),
|
|
342
|
+
};
|
|
260
343
|
}
|
|
261
344
|
|
|
262
345
|
// ── Executor Scheduler ───────────────────────────────────────────────────────
|
|
@@ -266,6 +349,7 @@ export class ExecutorScheduler {
|
|
|
266
349
|
this.executors = config.executors.filter((e) => e.enabled !== false);
|
|
267
350
|
this.failover = config.failover;
|
|
268
351
|
this.distribution = config.distribution;
|
|
352
|
+
this.workflowOffloadPolicy = normalizeWorkflowOffloadPolicy(config);
|
|
269
353
|
this._roundRobinIndex = 0;
|
|
270
354
|
this._failureCounts = new Map(); // name → consecutive failures
|
|
271
355
|
this._disabledUntil = new Map(); // name → timestamp
|
|
@@ -273,6 +357,45 @@ export class ExecutorScheduler {
|
|
|
273
357
|
this._workspaceConfigs = new Map(); // workspaceId → { maxConcurrent, pool, weight }
|
|
274
358
|
}
|
|
275
359
|
|
|
360
|
+
selectWorkflowLane({ nodeType = "", commandType = "", command = "" } = {}) {
|
|
361
|
+
const policy = this.workflowOffloadPolicy || DEFAULT_WORKFLOW_OFFLOAD_POLICY;
|
|
362
|
+
if (!policy.enabled) {
|
|
363
|
+
return { lane: "main", reason: "offload_disabled", heavy: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const normalizedNodeType = String(nodeType || "").trim();
|
|
367
|
+
if (normalizedNodeType && policy.nodeTypes.has(normalizedNodeType)) {
|
|
368
|
+
return {
|
|
369
|
+
lane: "isolated",
|
|
370
|
+
reason: `workflow_node:${normalizedNodeType}`,
|
|
371
|
+
heavy: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const normalizedCommandType = String(commandType || "").trim();
|
|
376
|
+
if (normalizedCommandType && policy.commandTypes.has(normalizedCommandType)) {
|
|
377
|
+
return {
|
|
378
|
+
lane: "isolated",
|
|
379
|
+
reason: `command_type:${normalizedCommandType}`,
|
|
380
|
+
heavy: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const rawCommand = String(command || "");
|
|
385
|
+
if (rawCommand) {
|
|
386
|
+
const matched = policy.commandPatterns.find((pattern) => pattern.test(rawCommand));
|
|
387
|
+
if (matched) {
|
|
388
|
+
return {
|
|
389
|
+
lane: "isolated",
|
|
390
|
+
reason: `command_pattern:${matched.source}`,
|
|
391
|
+
heavy: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { lane: "main", reason: "lightweight", heavy: false };
|
|
397
|
+
}
|
|
398
|
+
|
|
276
399
|
/**
|
|
277
400
|
* Register workspace executor config for concurrency tracking.
|
|
278
401
|
* @param {string} workspaceId
|