bosun 0.34.5 → 0.34.7
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/agent-hook-bridge.mjs +6 -6
- package/agent-pool.mjs +75 -5
- package/bosun.schema.json +31 -0
- package/cli.mjs +18 -3
- package/codex-config.mjs +256 -8
- package/config.mjs +54 -8
- package/copilot-shell.mjs +75 -5
- package/desktop/main.mjs +3 -0
- package/git-commit-helpers.mjs +31 -3
- package/library-manager.mjs +6 -4
- package/monitor.mjs +126 -11
- package/package.json +2 -1
- package/postinstall.mjs +8 -5
- package/primary-agent.mjs +128 -19
- package/repo-config.mjs +110 -7
- package/setup-web-server.mjs +4 -3
- package/setup.mjs +49 -57
- package/task-context.mjs +200 -0
- package/task-executor.mjs +142 -34
- package/telegram-bot.mjs +13 -4
- package/ui/app.js +1 -1
- package/ui/components/agent-selector.js +17 -7
- package/ui/components/chat-view.js +109 -79
- package/ui/components/diff-viewer.js +1 -1
- package/ui/components/workspace-switcher.js +4 -4
- package/ui/modules/agent-display.js +5 -5
- package/ui/setup.html +104 -22
- package/ui/styles/base.css +1 -1
- package/ui/styles/sessions.css +113 -0
- package/ui/styles/workspace-switcher.css +10 -0
- package/ui/tabs/agents.js +6 -6
- package/ui/tabs/chat.js +33 -33
- package/ui/tabs/dashboard.js +10 -10
- package/ui/tabs/library.js +18 -16
- package/ui/tabs/settings.js +161 -15
- package/ui/tabs/tasks.js +8 -8
- package/ui/tabs/workflows.js +3 -2
- package/ui-server.mjs +152 -21
- package/workflow-engine.mjs +14 -0
- package/workflow-nodes.mjs +537 -43
- package/workflow-templates/agents.mjs +70 -4
- package/workflow-templates/ci-cd.mjs +14 -7
- package/workflow-templates/github.mjs +21 -9
- package/workflow-templates/reliability.mjs +283 -0
- package/workflow-templates/security.mjs +5 -5
- package/workflow-templates.mjs +7 -1
package/agent-hook-bridge.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { executeBlockingHooks, executeHooks, loadHooks, registerBuiltinHooks } from "./agent-hooks.mjs";
|
|
5
|
+
import { shouldRunAgentHookBridge } from "./task-context.mjs";
|
|
5
6
|
|
|
6
7
|
const TAG = "[agent-hook-bridge]";
|
|
7
8
|
|
|
@@ -161,12 +162,11 @@ function mapEvents(sourceEvent, payload) {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
async function run() {
|
|
164
|
-
// ──
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
if (!process.env.VE_MANAGED && !process.env.BOSUN_HOOKS_FORCE) {
|
|
165
|
+
// ── task-context guard ────────────────────────────────────────────────────
|
|
166
|
+
// Execute hooks only for active Bosun task sessions.
|
|
167
|
+
// This prevents globally scaffolded hook configs from affecting standalone
|
|
168
|
+
// sessions in user repos. BOSUN_HOOKS_FORCE remains an explicit override.
|
|
169
|
+
if (!shouldRunAgentHookBridge(process.env)) {
|
|
170
170
|
process.exit(0);
|
|
171
171
|
}
|
|
172
172
|
|
package/agent-pool.mjs
CHANGED
|
@@ -45,6 +45,16 @@ import { loadConfig } from "./config.mjs";
|
|
|
45
45
|
import { resolveRepoRoot, resolveAgentRepoRoot } from "./repo-root.mjs";
|
|
46
46
|
import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
|
|
47
47
|
|
|
48
|
+
// Lazy-load MCP registry to avoid circular dependencies.
|
|
49
|
+
// Cached at module scope per AGENTS.md hard rules.
|
|
50
|
+
let _mcpRegistry = null;
|
|
51
|
+
async function getMcpRegistry() {
|
|
52
|
+
if (!_mcpRegistry) {
|
|
53
|
+
_mcpRegistry = await import("./mcp-registry.mjs");
|
|
54
|
+
}
|
|
55
|
+
return _mcpRegistry;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
const __filename = fileURLToPath(import.meta.url);
|
|
49
59
|
const __dirname = dirname(__filename);
|
|
50
60
|
|
|
@@ -897,7 +907,7 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
897
907
|
* Build CLI arguments for ephemeral Copilot agent-pool sessions.
|
|
898
908
|
* Mirrors copilot-shell.mjs buildCliArgs() for feature parity.
|
|
899
909
|
*/
|
|
900
|
-
function buildPoolCopilotCliArgs() {
|
|
910
|
+
function buildPoolCopilotCliArgs(mcpConfigPath) {
|
|
901
911
|
const args = [];
|
|
902
912
|
if (!envFlagEnabled(process.env.COPILOT_NO_EXPERIMENTAL)) {
|
|
903
913
|
args.push("--experimental");
|
|
@@ -915,9 +925,10 @@ function buildPoolCopilotCliArgs() {
|
|
|
915
925
|
if (envFlagEnabled(process.env.COPILOT_DISABLE_BUILTIN_MCPS)) {
|
|
916
926
|
args.push("--disable-builtin-mcps");
|
|
917
927
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
928
|
+
// MCP config: prefer per-launch resolved config, fall back to env var
|
|
929
|
+
const effectiveMcpConfig = mcpConfigPath || process.env.COPILOT_ADDITIONAL_MCP_CONFIG;
|
|
930
|
+
if (effectiveMcpConfig) {
|
|
931
|
+
args.push("--additional-mcp-config", effectiveMcpConfig);
|
|
921
932
|
}
|
|
922
933
|
return args;
|
|
923
934
|
}
|
|
@@ -1024,7 +1035,17 @@ async function launchCopilotThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1024
1035
|
clientOpts = { cliUrl, env: clientEnv };
|
|
1025
1036
|
} else {
|
|
1026
1037
|
// Local mode (default): stdio for full capability
|
|
1027
|
-
|
|
1038
|
+
// Write temp MCP config if resolved MCP servers are available
|
|
1039
|
+
let mcpConfigPath = null;
|
|
1040
|
+
if (extra._resolvedMcpServers?.length) {
|
|
1041
|
+
try {
|
|
1042
|
+
const registry = await getMcpRegistry();
|
|
1043
|
+
mcpConfigPath = registry.writeTempCopilotMcpConfig(cwd, extra._resolvedMcpServers);
|
|
1044
|
+
} catch (mcpErr) {
|
|
1045
|
+
console.warn(`${TAG} copilot MCP config write failed (non-fatal): ${mcpErr.message}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const cliArgs = buildPoolCopilotCliArgs(mcpConfigPath);
|
|
1028
1049
|
clientOpts = {
|
|
1029
1050
|
cwd,
|
|
1030
1051
|
env: clientEnv,
|
|
@@ -1465,6 +1486,27 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1465
1486
|
}
|
|
1466
1487
|
|
|
1467
1488
|
// ── 4. Execute query ─────────────────────────────────────────────────────
|
|
1489
|
+
// Inject MCP server config for Claude via environment variable
|
|
1490
|
+
let _claudeMcpEnvCleanup = null;
|
|
1491
|
+
if (extra._resolvedMcpServers?.length) {
|
|
1492
|
+
try {
|
|
1493
|
+
const registry = await getMcpRegistry();
|
|
1494
|
+
const { envVar } = registry.buildClaudeMcpEnv(extra._resolvedMcpServers);
|
|
1495
|
+
if (envVar) {
|
|
1496
|
+
const prev = process.env.CLAUDE_MCP_SERVERS;
|
|
1497
|
+
process.env.CLAUDE_MCP_SERVERS = envVar;
|
|
1498
|
+
_claudeMcpEnvCleanup = () => {
|
|
1499
|
+
if (prev === undefined) {
|
|
1500
|
+
delete process.env.CLAUDE_MCP_SERVERS;
|
|
1501
|
+
} else {
|
|
1502
|
+
process.env.CLAUDE_MCP_SERVERS = prev;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
} catch (mcpErr) {
|
|
1507
|
+
console.warn(`${TAG} claude MCP env setup failed (non-fatal): ${mcpErr.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1468
1510
|
try {
|
|
1469
1511
|
const msgQueue = createMessageQueue();
|
|
1470
1512
|
|
|
@@ -1596,6 +1638,7 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1596
1638
|
|
|
1597
1639
|
const output =
|
|
1598
1640
|
finalResponse.trim() || "(Agent completed with no text output)";
|
|
1641
|
+
if (typeof _claudeMcpEnvCleanup === "function") _claudeMcpEnvCleanup();
|
|
1599
1642
|
return {
|
|
1600
1643
|
success: true,
|
|
1601
1644
|
output,
|
|
@@ -1605,6 +1648,7 @@ async function launchClaudeThread(prompt, cwd, timeoutMs, extra = {}) {
|
|
|
1605
1648
|
threadId: activeClaudeSessionId,
|
|
1606
1649
|
};
|
|
1607
1650
|
} catch (err) {
|
|
1651
|
+
if (typeof _claudeMcpEnvCleanup === "function") _claudeMcpEnvCleanup();
|
|
1608
1652
|
clearAbortScope();
|
|
1609
1653
|
if (hardTimer) clearTimeout(hardTimer);
|
|
1610
1654
|
if (steerKey) unregisterActiveSession(steerKey);
|
|
@@ -1714,6 +1758,32 @@ export async function launchEphemeralThread(
|
|
|
1714
1758
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
1715
1759
|
extra = {},
|
|
1716
1760
|
) {
|
|
1761
|
+
// ── Resolve MCP servers for this launch ──────────────────────────────────
|
|
1762
|
+
try {
|
|
1763
|
+
if (!extra._mcpResolved) {
|
|
1764
|
+
const cfg = loadConfig();
|
|
1765
|
+
const mcpCfg = cfg.mcpServers || {};
|
|
1766
|
+
if (mcpCfg.enabled !== false) {
|
|
1767
|
+
const requestedIds = extra.mcpServers || [];
|
|
1768
|
+
const defaultIds = mcpCfg.defaultServers || [];
|
|
1769
|
+
if (requestedIds.length || defaultIds.length) {
|
|
1770
|
+
const registry = await getMcpRegistry();
|
|
1771
|
+
const resolved = await registry.resolveMcpServersForAgent(
|
|
1772
|
+
cwd,
|
|
1773
|
+
requestedIds,
|
|
1774
|
+
{ defaultServers: defaultIds, catalogOverrides: mcpCfg.catalogOverrides || {} },
|
|
1775
|
+
);
|
|
1776
|
+
if (resolved.length) {
|
|
1777
|
+
extra._resolvedMcpServers = resolved;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
extra._mcpResolved = true;
|
|
1782
|
+
}
|
|
1783
|
+
} catch (mcpErr) {
|
|
1784
|
+
console.warn(`${TAG} MCP server resolution failed (non-fatal): ${mcpErr.message}`);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1717
1787
|
// Determine the primary SDK to try
|
|
1718
1788
|
const requestedSdk = extra.sdk
|
|
1719
1789
|
? String(extra.sdk).trim().toLowerCase()
|
package/bosun.schema.json
CHANGED
|
@@ -606,6 +606,37 @@
|
|
|
606
606
|
"type": "object",
|
|
607
607
|
"description": "Hook definitions loaded by agent-hooks.mjs (event -> hook list).",
|
|
608
608
|
"additionalProperties": true
|
|
609
|
+
},
|
|
610
|
+
"mcpServers": {
|
|
611
|
+
"type": "object",
|
|
612
|
+
"description": "MCP (Model Context Protocol) server management. Controls which MCP servers are available to agents.",
|
|
613
|
+
"additionalProperties": false,
|
|
614
|
+
"properties": {
|
|
615
|
+
"enabled": {
|
|
616
|
+
"type": "boolean",
|
|
617
|
+
"default": true,
|
|
618
|
+
"description": "Enable MCP server integration for agent launches"
|
|
619
|
+
},
|
|
620
|
+
"defaultServers": {
|
|
621
|
+
"type": "array",
|
|
622
|
+
"items": { "type": "string" },
|
|
623
|
+
"description": "MCP server IDs attached to every agent launch by default (e.g. [\"context7\", \"microsoft-docs\"])"
|
|
624
|
+
},
|
|
625
|
+
"catalogOverrides": {
|
|
626
|
+
"type": "object",
|
|
627
|
+
"additionalProperties": {
|
|
628
|
+
"type": "object",
|
|
629
|
+
"description": "Per-server environment variable overrides",
|
|
630
|
+
"additionalProperties": { "type": "string" }
|
|
631
|
+
},
|
|
632
|
+
"description": "Environment variable overrides keyed by MCP server ID"
|
|
633
|
+
},
|
|
634
|
+
"autoInstallDefaults": {
|
|
635
|
+
"type": "boolean",
|
|
636
|
+
"default": true,
|
|
637
|
+
"description": "Automatically install defaultServers from catalog if not already installed"
|
|
638
|
+
}
|
|
639
|
+
}
|
|
609
640
|
}
|
|
610
641
|
}
|
|
611
642
|
}
|
package/cli.mjs
CHANGED
|
@@ -1484,14 +1484,21 @@ function detectExistingMonitorLockOwner(excludePid = null) {
|
|
|
1484
1484
|
return null;
|
|
1485
1485
|
}
|
|
1486
1486
|
|
|
1487
|
-
function runMonitor() {
|
|
1487
|
+
function runMonitor({ restartReason = "" } = {}) {
|
|
1488
1488
|
return new Promise((resolve, reject) => {
|
|
1489
1489
|
const monitorPath = fileURLToPath(
|
|
1490
1490
|
new URL("./monitor.mjs", import.meta.url),
|
|
1491
1491
|
);
|
|
1492
|
+
const childEnv = { ...process.env };
|
|
1493
|
+
if (restartReason) {
|
|
1494
|
+
childEnv.BOSUN_MONITOR_RESTART_REASON = restartReason;
|
|
1495
|
+
} else {
|
|
1496
|
+
delete childEnv.BOSUN_MONITOR_RESTART_REASON;
|
|
1497
|
+
}
|
|
1492
1498
|
monitorChild = fork(monitorPath, process.argv.slice(2), {
|
|
1493
1499
|
stdio: "inherit",
|
|
1494
1500
|
execArgv: ["--max-old-space-size=4096"],
|
|
1501
|
+
env: childEnv,
|
|
1495
1502
|
windowsHide: IS_DAEMON_CHILD && process.platform === "win32",
|
|
1496
1503
|
});
|
|
1497
1504
|
daemonCrashTracker.markStart();
|
|
@@ -1504,7 +1511,7 @@ function runMonitor() {
|
|
|
1504
1511
|
"\n \u21BB Monitor restarting with fresh modules...\n",
|
|
1505
1512
|
);
|
|
1506
1513
|
// Small delay to let file writes / port releases settle
|
|
1507
|
-
setTimeout(() => resolve(runMonitor()), 2000);
|
|
1514
|
+
setTimeout(() => resolve(runMonitor({ restartReason: "self-restart" })), 2000);
|
|
1508
1515
|
} else {
|
|
1509
1516
|
const exitCode = code ?? (signal ? 1 : 0);
|
|
1510
1517
|
const existingOwner =
|
|
@@ -1572,7 +1579,15 @@ function runMonitor() {
|
|
|
1572
1579
|
restartAttempt: daemonRestartCount,
|
|
1573
1580
|
maxRestarts: IS_DAEMON_CHILD ? DAEMON_MAX_RESTARTS : 0,
|
|
1574
1581
|
}).catch(() => {});
|
|
1575
|
-
setTimeout(
|
|
1582
|
+
setTimeout(
|
|
1583
|
+
() =>
|
|
1584
|
+
resolve(
|
|
1585
|
+
runMonitor({
|
|
1586
|
+
restartReason: isOSKill ? "os-kill" : "crash",
|
|
1587
|
+
}),
|
|
1588
|
+
),
|
|
1589
|
+
delayMs,
|
|
1590
|
+
);
|
|
1576
1591
|
return;
|
|
1577
1592
|
}
|
|
1578
1593
|
|
package/codex-config.mjs
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* codex-config.mjs — Manages the Codex CLI config (~/.codex/config.toml)
|
|
3
3
|
*
|
|
4
4
|
* Ensures the user's Codex CLI configuration has:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
10
|
-
*
|
|
5
|
+
* 1. Sufficient stream_idle_timeout_ms on all model providers
|
|
6
|
+
* 2. Recommended defaults for long-running agentic workloads
|
|
7
|
+
* 3. Feature flags for sub-agents, memory, undo, collaboration
|
|
8
|
+
* 4. Sandbox permissions and shell environment policy
|
|
9
|
+
* 5. Common MCP servers (context7, microsoft-docs)
|
|
10
|
+
*
|
|
11
|
+
* NOTE: Vibe-Kanban MCP is workspace-scoped and managed by repo-config.mjs
|
|
12
|
+
* inside each repo's `.codex/config.toml`. Global config no longer auto-adds
|
|
13
|
+
* `[mcp_servers.vibe_kanban]`.
|
|
11
14
|
*
|
|
12
15
|
* SCOPE: This manages the GLOBAL ~/.codex/config.toml which contains:
|
|
13
16
|
* - Model provider configs (API keys, base URLs) — MUST be global
|
|
@@ -887,14 +890,17 @@ export function buildCommonMcpBlocks() {
|
|
|
887
890
|
"",
|
|
888
891
|
"# ── Common MCP servers (added by bosun) ──",
|
|
889
892
|
"[mcp_servers.context7]",
|
|
893
|
+
"startup_timeout_sec = 120",
|
|
890
894
|
'command = "npx"',
|
|
891
895
|
'args = ["-y", "@upstash/context7-mcp"]',
|
|
892
896
|
"",
|
|
893
897
|
"[mcp_servers.sequential-thinking]",
|
|
898
|
+
"startup_timeout_sec = 120",
|
|
894
899
|
'command = "npx"',
|
|
895
900
|
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
896
901
|
"",
|
|
897
902
|
"[mcp_servers.playwright]",
|
|
903
|
+
"startup_timeout_sec = 120",
|
|
898
904
|
'command = "npx"',
|
|
899
905
|
'args = ["-y", "@playwright/mcp@latest"]',
|
|
900
906
|
"",
|
|
@@ -914,6 +920,44 @@ function hasNamedMcpServer(toml, name) {
|
|
|
914
920
|
);
|
|
915
921
|
}
|
|
916
922
|
|
|
923
|
+
function ensureMcpStartupTimeout(toml, name, timeoutSec = 120) {
|
|
924
|
+
const header = `[mcp_servers.${name}]`;
|
|
925
|
+
const headerIdx = toml.indexOf(header);
|
|
926
|
+
if (headerIdx === -1) return { toml, changed: false };
|
|
927
|
+
|
|
928
|
+
const afterHeader = headerIdx + header.length;
|
|
929
|
+
const nextSection = toml.indexOf("\n[", afterHeader);
|
|
930
|
+
const sectionEnd = nextSection === -1 ? toml.length : nextSection;
|
|
931
|
+
let section = toml.substring(afterHeader, sectionEnd);
|
|
932
|
+
|
|
933
|
+
const timeoutRegex = /^startup_timeout_sec\s*=\s*\d+.*$/m;
|
|
934
|
+
let changed = false;
|
|
935
|
+
if (timeoutRegex.test(section)) {
|
|
936
|
+
const desired = `startup_timeout_sec = ${timeoutSec}`;
|
|
937
|
+
const updated = section.replace(timeoutRegex, desired);
|
|
938
|
+
if (updated !== section) {
|
|
939
|
+
section = updated;
|
|
940
|
+
changed = true;
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
section = section.trimEnd() + `\nstartup_timeout_sec = ${timeoutSec}\n`;
|
|
944
|
+
changed = true;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (!changed) return { toml, changed: false };
|
|
948
|
+
return {
|
|
949
|
+
toml: toml.substring(0, afterHeader) + section + toml.substring(sectionEnd),
|
|
950
|
+
changed: true,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function stripDeprecatedSandboxPermissions(toml) {
|
|
955
|
+
return String(toml || "").replace(
|
|
956
|
+
/^\s*sandbox_permissions\s*=.*(?:\r?\n)?/gim,
|
|
957
|
+
"",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
917
961
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
918
962
|
|
|
919
963
|
/**
|
|
@@ -1264,13 +1308,15 @@ export function ensureRetrySettings(toml, providerName) {
|
|
|
1264
1308
|
* @param {object} opts
|
|
1265
1309
|
* @param {string} [opts.vkBaseUrl]
|
|
1266
1310
|
* @param {boolean} [opts.skipVk]
|
|
1311
|
+
* @param {boolean} [opts.manageVkMcp] Explicit opt-in to manage VK MCP in global config
|
|
1267
1312
|
* @param {boolean} [opts.dryRun] If true, returns result without writing
|
|
1268
1313
|
* @param {object} [opts.env] Environment overrides (defaults to process.env)
|
|
1269
1314
|
* @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
|
|
1270
1315
|
*/
|
|
1271
1316
|
export function ensureCodexConfig({
|
|
1272
1317
|
vkBaseUrl = "http://127.0.0.1:54089",
|
|
1273
|
-
skipVk =
|
|
1318
|
+
skipVk = true,
|
|
1319
|
+
manageVkMcp = false,
|
|
1274
1320
|
dryRun = false,
|
|
1275
1321
|
env = process.env,
|
|
1276
1322
|
primarySdk,
|
|
@@ -1298,6 +1344,208 @@ export function ensureCodexConfig({
|
|
|
1298
1344
|
noChanges: true,
|
|
1299
1345
|
};
|
|
1300
1346
|
|
|
1347
|
+
const configExisted = existsSync(CONFIG_PATH);
|
|
1348
|
+
const originalToml = readCodexConfig();
|
|
1349
|
+
let toml = stripDeprecatedSandboxPermissions(originalToml);
|
|
1350
|
+
if (!configExisted) {
|
|
1351
|
+
result.created = true;
|
|
1352
|
+
toml = "";
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const sandboxModeResult = ensureTopLevelSandboxMode(
|
|
1356
|
+
toml,
|
|
1357
|
+
env.CODEX_SANDBOX_MODE,
|
|
1358
|
+
);
|
|
1359
|
+
toml = sandboxModeResult.toml;
|
|
1360
|
+
if (sandboxModeResult.changed) {
|
|
1361
|
+
result.sandboxAdded = true;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const repoRoot =
|
|
1365
|
+
env.BOSUN_AGENT_REPO_ROOT ||
|
|
1366
|
+
env.REPO_ROOT ||
|
|
1367
|
+
env.BOSUN_HOME ||
|
|
1368
|
+
process.cwd();
|
|
1369
|
+
const additionalRoots = env.BOSUN_WORKSPACES_DIR
|
|
1370
|
+
? [env.BOSUN_WORKSPACES_DIR]
|
|
1371
|
+
: [];
|
|
1372
|
+
const sandboxWorkspaceResult = ensureSandboxWorkspaceWrite(toml, {
|
|
1373
|
+
repoRoot,
|
|
1374
|
+
additionalRoots,
|
|
1375
|
+
writableRoots: env.CODEX_SANDBOX_WRITABLE_ROOTS,
|
|
1376
|
+
});
|
|
1377
|
+
toml = sandboxWorkspaceResult.toml;
|
|
1378
|
+
result.sandboxWorkspaceAdded = sandboxWorkspaceResult.added;
|
|
1379
|
+
result.sandboxWorkspaceUpdated =
|
|
1380
|
+
sandboxWorkspaceResult.changed && !sandboxWorkspaceResult.added;
|
|
1381
|
+
result.sandboxWorkspaceRootsAdded = sandboxWorkspaceResult.rootsAdded;
|
|
1382
|
+
|
|
1383
|
+
const pruneResult = pruneStaleSandboxRoots(toml);
|
|
1384
|
+
toml = pruneResult.toml;
|
|
1385
|
+
result.sandboxStaleRootsRemoved = pruneResult.removed;
|
|
1386
|
+
|
|
1387
|
+
if (!hasShellEnvPolicy(toml)) {
|
|
1388
|
+
toml += buildShellEnvPolicy(env.CODEX_SHELL_ENV_POLICY || "all");
|
|
1389
|
+
result.shellEnvAdded = true;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const rawPrimary = String(primarySdk || env.PRIMARY_AGENT || "codex")
|
|
1393
|
+
.trim()
|
|
1394
|
+
.toLowerCase();
|
|
1395
|
+
const normalizedPrimary =
|
|
1396
|
+
rawPrimary === "copilot" || rawPrimary.includes("copilot")
|
|
1397
|
+
? "copilot"
|
|
1398
|
+
: rawPrimary === "claude" || rawPrimary.includes("claude")
|
|
1399
|
+
? "claude"
|
|
1400
|
+
: rawPrimary === "codex" || rawPrimary.includes("codex")
|
|
1401
|
+
? "codex"
|
|
1402
|
+
: "codex";
|
|
1403
|
+
if (!hasAgentSdkConfig(toml)) {
|
|
1404
|
+
toml += buildAgentSdkBlock({ primary: normalizedPrimary });
|
|
1405
|
+
result.agentSdkAdded = true;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const maxThreads = resolveAgentMaxThreads(env);
|
|
1409
|
+
if (maxThreads.explicit && !maxThreads.value) {
|
|
1410
|
+
result.agentMaxThreadsSkipped = String(maxThreads.raw);
|
|
1411
|
+
} else {
|
|
1412
|
+
const maxThreadsResult = ensureAgentMaxThreads(toml, {
|
|
1413
|
+
maxThreads: maxThreads.value,
|
|
1414
|
+
overwrite: maxThreads.explicit,
|
|
1415
|
+
});
|
|
1416
|
+
toml = maxThreadsResult.toml;
|
|
1417
|
+
if (maxThreadsResult.changed && !maxThreadsResult.skipped) {
|
|
1418
|
+
result.agentMaxThreads = {
|
|
1419
|
+
from: maxThreadsResult.existing,
|
|
1420
|
+
to: maxThreadsResult.applied,
|
|
1421
|
+
explicit: maxThreads.explicit,
|
|
1422
|
+
};
|
|
1423
|
+
} else if (maxThreadsResult.skipped && maxThreads.explicit) {
|
|
1424
|
+
result.agentMaxThreadsSkipped = String(maxThreads.raw);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const featureResult = ensureFeatureFlags(toml, env);
|
|
1429
|
+
result.featuresAdded = featureResult.added;
|
|
1430
|
+
toml = featureResult.toml;
|
|
1431
|
+
|
|
1432
|
+
const shouldManageGlobalVkMcp = Boolean(manageVkMcp) && !skipVk;
|
|
1433
|
+
if (!shouldManageGlobalVkMcp) {
|
|
1434
|
+
if (hasVibeKanbanMcp(toml)) {
|
|
1435
|
+
toml = removeVibeKanbanMcp(toml);
|
|
1436
|
+
result.vkRemoved = true;
|
|
1437
|
+
}
|
|
1438
|
+
} else if (!hasVibeKanbanMcp(toml)) {
|
|
1439
|
+
toml += buildVibeKanbanBlock({ vkBaseUrl });
|
|
1440
|
+
result.vkAdded = true;
|
|
1441
|
+
} else {
|
|
1442
|
+
const vkEnvValues = {
|
|
1443
|
+
VK_BASE_URL: vkBaseUrl,
|
|
1444
|
+
VK_ENDPOINT_URL: vkBaseUrl,
|
|
1445
|
+
};
|
|
1446
|
+
const beforeVkEnv = toml;
|
|
1447
|
+
if (!hasVibeKanbanEnv(toml)) {
|
|
1448
|
+
toml =
|
|
1449
|
+
toml.trimEnd() +
|
|
1450
|
+
"\n\n[mcp_servers.vibe_kanban.env]\n" +
|
|
1451
|
+
`VK_BASE_URL = "${vkBaseUrl}"\n` +
|
|
1452
|
+
`VK_ENDPOINT_URL = "${vkBaseUrl}"\n`;
|
|
1453
|
+
} else {
|
|
1454
|
+
toml = updateVibeKanbanEnv(toml, vkEnvValues);
|
|
1455
|
+
}
|
|
1456
|
+
if (toml !== beforeVkEnv) {
|
|
1457
|
+
result.vkEnvUpdated = true;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const commonMcpBlocks = [
|
|
1462
|
+
{
|
|
1463
|
+
present: hasContext7Mcp(toml),
|
|
1464
|
+
block: [
|
|
1465
|
+
"",
|
|
1466
|
+
"# ── Common MCP servers (added by bosun) ──",
|
|
1467
|
+
"[mcp_servers.context7]",
|
|
1468
|
+
"startup_timeout_sec = 120",
|
|
1469
|
+
'command = "npx"',
|
|
1470
|
+
'args = ["-y", "@upstash/context7-mcp"]',
|
|
1471
|
+
"",
|
|
1472
|
+
].join("\n"),
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
present: hasNamedMcpServer(toml, "sequential-thinking"),
|
|
1476
|
+
block: [
|
|
1477
|
+
"",
|
|
1478
|
+
"[mcp_servers.sequential-thinking]",
|
|
1479
|
+
"startup_timeout_sec = 120",
|
|
1480
|
+
'command = "npx"',
|
|
1481
|
+
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
|
|
1482
|
+
"",
|
|
1483
|
+
].join("\n"),
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
present: hasNamedMcpServer(toml, "playwright"),
|
|
1487
|
+
block: [
|
|
1488
|
+
"",
|
|
1489
|
+
"[mcp_servers.playwright]",
|
|
1490
|
+
"startup_timeout_sec = 120",
|
|
1491
|
+
'command = "npx"',
|
|
1492
|
+
'args = ["-y", "@playwright/mcp@latest"]',
|
|
1493
|
+
"",
|
|
1494
|
+
].join("\n"),
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
present: hasMicrosoftDocsMcp(toml),
|
|
1498
|
+
block: [
|
|
1499
|
+
"",
|
|
1500
|
+
"[mcp_servers.microsoft-docs]",
|
|
1501
|
+
'url = "https://learn.microsoft.com/api/mcp"',
|
|
1502
|
+
'tools = ["microsoft_docs_search", "microsoft_code_sample_search"]',
|
|
1503
|
+
"",
|
|
1504
|
+
].join("\n"),
|
|
1505
|
+
},
|
|
1506
|
+
];
|
|
1507
|
+
for (const item of commonMcpBlocks) {
|
|
1508
|
+
if (item.present) continue;
|
|
1509
|
+
toml += item.block;
|
|
1510
|
+
result.commonMcpAdded = true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
for (const serverName of ["context7", "sequential-thinking", "playwright"]) {
|
|
1514
|
+
const timeoutResult = ensureMcpStartupTimeout(toml, serverName, 120);
|
|
1515
|
+
toml = timeoutResult.toml;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const providerResult = ensureModelProviderSectionsFromEnv(toml, env);
|
|
1519
|
+
toml = providerResult.toml;
|
|
1520
|
+
result.profileProvidersAdded = providerResult.added;
|
|
1521
|
+
|
|
1522
|
+
const timeoutAudit = auditStreamTimeouts(toml);
|
|
1523
|
+
for (const item of timeoutAudit) {
|
|
1524
|
+
if (!item.needsUpdate) continue;
|
|
1525
|
+
toml = setStreamTimeout(toml, item.provider, RECOMMENDED_STREAM_IDLE_TIMEOUT_MS);
|
|
1526
|
+
result.timeoutsFixed.push({
|
|
1527
|
+
provider: item.provider,
|
|
1528
|
+
from: item.currentValue,
|
|
1529
|
+
to: RECOMMENDED_STREAM_IDLE_TIMEOUT_MS,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const providers = auditStreamTimeouts(toml).map((item) => item.provider);
|
|
1534
|
+
for (const provider of providers) {
|
|
1535
|
+
const beforeRetry = toml;
|
|
1536
|
+
toml = ensureRetrySettings(toml, provider);
|
|
1537
|
+
if (toml !== beforeRetry) {
|
|
1538
|
+
result.retriesAdded.push(provider);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const changed = toml !== originalToml;
|
|
1543
|
+
result.noChanges = !result.created && !changed;
|
|
1544
|
+
|
|
1545
|
+
if (!dryRun && (result.created || changed)) {
|
|
1546
|
+
writeCodexConfig(toml);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1301
1549
|
return result;
|
|
1302
1550
|
}
|
|
1303
1551
|
|
|
@@ -1322,7 +1570,7 @@ export function printConfigSummary(result, log = console.log) {
|
|
|
1322
1570
|
}
|
|
1323
1571
|
|
|
1324
1572
|
if (result.vkRemoved) {
|
|
1325
|
-
log(" 🗑️ Removed Vibe-Kanban MCP server
|
|
1573
|
+
log(" 🗑️ Removed Vibe-Kanban MCP server from global config (workspace-scoped only)");
|
|
1326
1574
|
}
|
|
1327
1575
|
|
|
1328
1576
|
if (result.vkEnvUpdated) {
|
package/config.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { applyAllCompatibility } from "./compat.mjs";
|
|
|
29
29
|
import {
|
|
30
30
|
normalizeExecutorKey,
|
|
31
31
|
getModelsForExecutor,
|
|
32
|
+
MODEL_ALIASES,
|
|
32
33
|
} from "./task-complexity.mjs";
|
|
33
34
|
|
|
34
35
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -408,13 +409,43 @@ function parseListValue(value) {
|
|
|
408
409
|
.filter(Boolean);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
function
|
|
412
|
+
function inferExecutorModelsFromVariant(executor, variant) {
|
|
413
|
+
const normalizedExecutor = normalizeExecutorKey(executor);
|
|
414
|
+
if (!normalizedExecutor) return [];
|
|
415
|
+
const normalizedVariant = String(variant || "DEFAULT")
|
|
416
|
+
.trim()
|
|
417
|
+
.toUpperCase();
|
|
418
|
+
if (!normalizedVariant || normalizedVariant === "DEFAULT") return [];
|
|
419
|
+
|
|
420
|
+
const known = getModelsForExecutor(normalizedExecutor);
|
|
421
|
+
const inferred = known.filter((model) => {
|
|
422
|
+
const alias = MODEL_ALIASES[model];
|
|
423
|
+
return (
|
|
424
|
+
String(alias?.variant || "")
|
|
425
|
+
.trim()
|
|
426
|
+
.toUpperCase() === normalizedVariant
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
if (inferred.length > 0) return inferred;
|
|
430
|
+
|
|
431
|
+
// Fallback for variants encoded as model slug with underscores.
|
|
432
|
+
const slugGuess = normalizedVariant.toLowerCase().replaceAll("_", "-");
|
|
433
|
+
if (known.includes(slugGuess)) return [slugGuess];
|
|
434
|
+
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function normalizeExecutorModels(executor, models, variant = "DEFAULT") {
|
|
412
439
|
const normalizedExecutor = normalizeExecutorKey(executor);
|
|
413
440
|
if (!normalizedExecutor) return [];
|
|
414
441
|
const input = parseListValue(models);
|
|
415
442
|
const known = new Set(getModelsForExecutor(normalizedExecutor));
|
|
416
443
|
if (input.length === 0) {
|
|
417
|
-
|
|
444
|
+
const inferred = inferExecutorModelsFromVariant(
|
|
445
|
+
normalizedExecutor,
|
|
446
|
+
variant,
|
|
447
|
+
);
|
|
448
|
+
return inferred.length > 0 ? inferred : [...known];
|
|
418
449
|
}
|
|
419
450
|
return input.filter((model) => known.has(model));
|
|
420
451
|
}
|
|
@@ -433,7 +464,7 @@ function normalizeExecutorEntry(entry, index = 0, total = 1) {
|
|
|
433
464
|
const name =
|
|
434
465
|
String(entry.name || "").trim() ||
|
|
435
466
|
`${normalized}-${String(variant || "default").toLowerCase()}`;
|
|
436
|
-
const models = normalizeExecutorModels(executorType, entry.models);
|
|
467
|
+
const models = normalizeExecutorModels(executorType, entry.models, variant);
|
|
437
468
|
const codexProfile = String(
|
|
438
469
|
entry.codexProfile || entry.modelProfile || "",
|
|
439
470
|
).trim();
|
|
@@ -723,7 +754,11 @@ function parseExecutorsFromEnv() {
|
|
|
723
754
|
const parts = entries[i].split(":");
|
|
724
755
|
if (parts.length < 2) continue;
|
|
725
756
|
const executorType = parts[0].toUpperCase();
|
|
726
|
-
const models = normalizeExecutorModels(
|
|
757
|
+
const models = normalizeExecutorModels(
|
|
758
|
+
executorType,
|
|
759
|
+
parts[3] || "",
|
|
760
|
+
parts[1] || "DEFAULT",
|
|
761
|
+
);
|
|
727
762
|
executors.push({
|
|
728
763
|
name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
|
|
729
764
|
executor: executorType,
|
|
@@ -857,12 +892,23 @@ function loadExecutorConfig(configDir, configData) {
|
|
|
857
892
|
|
|
858
893
|
for (let index = 0; index < executors.length; index++) {
|
|
859
894
|
const current = executors[index];
|
|
860
|
-
if (current.codexProfile) continue;
|
|
861
895
|
const match = findExecutorMetadataMatch(current, fileExecutors, index);
|
|
862
|
-
if (!match
|
|
896
|
+
if (!match) continue;
|
|
897
|
+
const merged = { ...current };
|
|
898
|
+
if (typeof match.name === "string" && match.name.trim()) {
|
|
899
|
+
merged.name = match.name.trim();
|
|
900
|
+
}
|
|
901
|
+
if (typeof match.enabled === "boolean") {
|
|
902
|
+
merged.enabled = match.enabled;
|
|
903
|
+
}
|
|
904
|
+
if (Array.isArray(match.models) && match.models.length > 0) {
|
|
905
|
+
merged.models = [...new Set(match.models)];
|
|
906
|
+
}
|
|
907
|
+
if (match.codexProfile) {
|
|
908
|
+
merged.codexProfile = match.codexProfile;
|
|
909
|
+
}
|
|
863
910
|
executors[index] = {
|
|
864
|
-
...
|
|
865
|
-
codexProfile: match.codexProfile,
|
|
911
|
+
...merged,
|
|
866
912
|
};
|
|
867
913
|
}
|
|
868
914
|
}
|