bosun 0.34.6 → 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.
@@ -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
- // ── VE_MANAGED guard ──────────────────────────────────────────────────────
165
- // Only execute hooks for sessions managed by bosun.
166
- // bosun sets VE_MANAGED=1 in all spawned agent environments.
167
- // If this env var is missing, we're running inside a standalone agent session
168
- // that just happens to have the hook files in its config — exit silently.
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
- const mcpConfigPath = process.env.COPILOT_ADDITIONAL_MCP_CONFIG;
919
- if (mcpConfigPath) {
920
- args.push("--additional-mcp-config", mcpConfigPath);
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
- const cliArgs = buildPoolCopilotCliArgs();
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(() => resolve(runMonitor()), delayMs);
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. A vibe_kanban MCP server section with the correct env vars
6
- * 2. Sufficient stream_idle_timeout_ms on all model providers
7
- * 3. Recommended defaults for long-running agentic workloads
8
- * 4. Feature flags for sub-agents, memory, undo, collaboration
9
- * 5. Sandbox permissions and shell environment policy
10
- * 6. Common MCP servers (context7, microsoft-docs)
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
@@ -1305,13 +1308,15 @@ export function ensureRetrySettings(toml, providerName) {
1305
1308
  * @param {object} opts
1306
1309
  * @param {string} [opts.vkBaseUrl]
1307
1310
  * @param {boolean} [opts.skipVk]
1311
+ * @param {boolean} [opts.manageVkMcp] Explicit opt-in to manage VK MCP in global config
1308
1312
  * @param {boolean} [opts.dryRun] If true, returns result without writing
1309
1313
  * @param {object} [opts.env] Environment overrides (defaults to process.env)
1310
1314
  * @param {string} [opts.primarySdk] Primary agent SDK: "codex", "copilot", or "claude"
1311
1315
  */
1312
1316
  export function ensureCodexConfig({
1313
1317
  vkBaseUrl = "http://127.0.0.1:54089",
1314
- skipVk = false,
1318
+ skipVk = true,
1319
+ manageVkMcp = false,
1315
1320
  dryRun = false,
1316
1321
  env = process.env,
1317
1322
  primarySdk,
@@ -1424,7 +1429,8 @@ export function ensureCodexConfig({
1424
1429
  result.featuresAdded = featureResult.added;
1425
1430
  toml = featureResult.toml;
1426
1431
 
1427
- if (skipVk) {
1432
+ const shouldManageGlobalVkMcp = Boolean(manageVkMcp) && !skipVk;
1433
+ if (!shouldManageGlobalVkMcp) {
1428
1434
  if (hasVibeKanbanMcp(toml)) {
1429
1435
  toml = removeVibeKanbanMcp(toml);
1430
1436
  result.vkRemoved = true;
@@ -1564,7 +1570,7 @@ export function printConfigSummary(result, log = console.log) {
1564
1570
  }
1565
1571
 
1566
1572
  if (result.vkRemoved) {
1567
- log(" 🗑️ Removed Vibe-Kanban MCP server (VK backend not active)");
1573
+ log(" 🗑️ Removed Vibe-Kanban MCP server from global config (workspace-scoped only)");
1568
1574
  }
1569
1575
 
1570
1576
  if (result.vkEnvUpdated) {
package/copilot-shell.mjs CHANGED
@@ -17,6 +17,15 @@ import { getGitHubToken } from "./github-auth-manager.mjs";
17
17
 
18
18
  const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
19
19
 
20
+ // Lazy-import MCP registry — cached at module scope per AGENTS.md rules.
21
+ let _mcpRegistry = null;
22
+ async function getMcpRegistry() {
23
+ if (!_mcpRegistry) {
24
+ _mcpRegistry = await import("./mcp-registry.mjs");
25
+ }
26
+ return _mcpRegistry;
27
+ }
28
+
20
29
  // ── Configuration ────────────────────────────────────────────────────────────
21
30
 
22
31
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic tasks
@@ -174,7 +183,7 @@ function logSessionEvent(logPath, event) {
174
183
  * Enables experimental features (fleet, autopilot), auto-permissions,
175
184
  * sub-agents, and autonomy.
176
185
  */
177
- function buildCliArgs() {
186
+ async function buildCliArgs() {
178
187
  const args = [];
179
188
 
180
189
  // Always enable experimental features (fleet, autopilot, persisted permissions, etc.)
@@ -218,6 +227,26 @@ function buildCliArgs() {
218
227
  args.push("--additional-mcp-config", mcpConfigPath);
219
228
  }
220
229
 
230
+ // Also write a temp MCP config from the library if installed servers exist
231
+ // (non-fatal: library MCP is a convenience, not a hard requirement)
232
+ if (!mcpConfigPath) {
233
+ try {
234
+ const registry = await getMcpRegistry();
235
+ const installed = await registry.listInstalledMcpServers(REPO_ROOT);
236
+ if (installed && installed.length) {
237
+ const ids = installed.map((e) => e.id);
238
+ const resolved = await registry.resolveMcpServersForAgent(REPO_ROOT, ids);
239
+ if (resolved && resolved.length) {
240
+ const tmpPath = registry.writeTempCopilotMcpConfig(REPO_ROOT, resolved);
241
+ args.push("--additional-mcp-config", tmpPath);
242
+ console.log(`[copilot-shell] injected ${resolved.length} library MCP server(s) via CLI args`);
243
+ }
244
+ }
245
+ } catch (err) {
246
+ console.warn(`[copilot-shell] failed to inject library MCP servers into CLI args: ${err.message}`);
247
+ }
248
+ }
249
+
221
250
  if (args.length > 0) {
222
251
  console.log(`[copilot-shell] cliArgs: ${args.join(" ")}`);
223
252
  }
@@ -399,7 +428,7 @@ async function ensureClientStarted() {
399
428
  const sessionMode = (process.env.COPILOT_SESSION_MODE || "local").trim().toLowerCase();
400
429
 
401
430
  // Build cliArgs for experimental features, permissions, and autonomy
402
- const cliArgs = buildCliArgs();
431
+ const cliArgs = await buildCliArgs();
403
432
 
404
433
  let clientOptions;
405
434
  if (transport === "url") {
@@ -553,7 +582,46 @@ function loadMcpServers(profile = null) {
553
582
  return loadMcpServersFromFile(configPath);
554
583
  }
555
584
 
556
- function buildSessionConfig() {
585
+ /**
586
+ * Merge installed MCP library servers into an existing mcpServers map.
587
+ * Called during session build to inject library-managed MCP servers into
588
+ * the Copilot SDK session alongside any profile/env servers.
589
+ *
590
+ * Non-fatal: if the registry is unavailable or encounters errors, the
591
+ * original servers map is returned unchanged.
592
+ *
593
+ * @param {Object|null} existingServers — mcpServers from profile/env/config
594
+ * @returns {Promise<Object|null>} — merged servers map
595
+ */
596
+ async function mergeLibraryMcpServers(existingServers) {
597
+ try {
598
+ const registry = await getMcpRegistry();
599
+ const installed = await registry.listInstalledMcpServers(REPO_ROOT);
600
+ if (!installed || !installed.length) return existingServers;
601
+
602
+ // Resolve all installed servers into full configs
603
+ const installedIds = installed.map((e) => e.id);
604
+ const resolved = await registry.resolveMcpServersForAgent(REPO_ROOT, installedIds);
605
+ if (!resolved || !resolved.length) return existingServers;
606
+
607
+ // Convert to Copilot mcpServers format: { [id]: { command, args, env? } | { url } }
608
+ const copilotJson = registry.buildCopilotMcpJson(resolved);
609
+ const libraryServers = copilotJson?.mcpServers || {};
610
+ if (!Object.keys(libraryServers).length) return existingServers;
611
+
612
+ // Merge: existing servers take precedence over library ones (user overrides win)
613
+ const merged = { ...libraryServers, ...(existingServers || {}) };
614
+ console.log(
615
+ `[copilot-shell] Merged ${Object.keys(libraryServers).length} library MCP server(s) into session`,
616
+ );
617
+ return merged;
618
+ } catch (err) {
619
+ console.warn(`[copilot-shell] Failed to merge library MCP servers: ${err.message}`);
620
+ return existingServers;
621
+ }
622
+ }
623
+
624
+ async function buildSessionConfig() {
557
625
  const profile = resolveCopilotProfile();
558
626
  const config = {
559
627
  streaming: true,
@@ -588,7 +656,9 @@ function buildSessionConfig() {
588
656
  config.reasoningEffort = effort.toLowerCase();
589
657
  }
590
658
 
591
- const mcpServers = loadMcpServers(profile);
659
+ // Load MCP servers from profile/env/config, then merge library-managed servers
660
+ const baseServers = loadMcpServers(profile);
661
+ const mcpServers = await mergeLibraryMcpServers(baseServers);
592
662
  if (mcpServers) config.mcpServers = mcpServers;
593
663
  return config;
594
664
  }
@@ -600,7 +670,7 @@ async function getSession() {
600
670
  const started = await ensureClientStarted();
601
671
  if (!started) throw new Error("Copilot SDK not available");
602
672
 
603
- const config = buildSessionConfig();
673
+ const config = await buildSessionConfig();
604
674
 
605
675
  if (activeSessionId && typeof copilotClient?.resumeSession === "function") {
606
676
  try {
package/desktop/main.mjs CHANGED
@@ -256,6 +256,9 @@ async function startUiServer() {
256
256
  if (uiServerStarted) return;
257
257
  const api = await loadUiServerModule();
258
258
  const server = await api.startTelegramUiServer({
259
+ host: "127.0.0.1",
260
+ publicHost: "127.0.0.1",
261
+ skipAutoOpen: true,
259
262
  dependencies: {
260
263
  configDir: resolveDesktopConfigDir(),
261
264
  },
@@ -9,6 +9,8 @@
9
9
  * GitHub appearance: https://github.com/apps/bosun-ve
10
10
  */
11
11
 
12
+ import { shouldAddBosunCoAuthor as shouldAddBosunCoAuthorByContext } from "./task-context.mjs";
13
+
12
14
  const BOSUN_BOT_TRAILER =
13
15
  "Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>";
14
16
 
@@ -38,17 +40,25 @@ export function appendBosunCoAuthor(message) {
38
40
  * @param {string} title - commit title (first line / summary)
39
41
  * @param {string} [body] - commit body (optional extended description)
40
42
  * @param {Object} [opts]
41
- * @param {boolean} [opts.addBosunCredit=true] - whether to append the co-author trailer
43
+ * @param {boolean} [opts.addBosunCredit] - whether to append the co-author trailer
44
+ * @param {string} [opts.taskId] - optional task ID for task-scoped attribution mode
45
+ * @param {NodeJS.ProcessEnv} [opts.env] - optional environment override
42
46
  * @returns {string} full commit message
43
47
  */
44
- export function buildCommitMessage(title, body = "", { addBosunCredit = true } = {}) {
48
+ export function buildCommitMessage(title, body = "", opts = {}) {
49
+ const { addBosunCredit, taskId, env } = opts;
45
50
  const parts = [title.trimEnd()];
46
51
  if (body && body.trim()) {
47
52
  parts.push(""); // blank line
48
53
  parts.push(body.trimEnd());
49
54
  }
50
55
  const base = parts.join("\n");
51
- return addBosunCredit ? appendBosunCoAuthor(base) : base;
56
+
57
+ const withBosunCredit =
58
+ typeof addBosunCredit === "boolean"
59
+ ? addBosunCredit
60
+ : shouldAddBosunCoAuthor({ taskId, env });
61
+ return withBosunCredit ? appendBosunCoAuthor(base) : base;
52
62
  }
53
63
 
54
64
  // ── PR body helpers ───────────────────────────────────────────────────────────
@@ -81,3 +91,21 @@ export function getBosunCoAuthorTrailer() {
81
91
  export function getBosunPrCredit() {
82
92
  return BOSUN_PR_CREDIT;
83
93
  }
94
+
95
+ /**
96
+ * Returns whether Bosun co-author credit should be applied for the current
97
+ * execution context.
98
+ *
99
+ * Defaults to task-scoped behavior and supports opt-in overrides via:
100
+ * - BOSUN_COAUTHOR_MODE=always
101
+ * - BOSUN_COAUTHOR_MODE=off
102
+ *
103
+ * @param {object} [options]
104
+ * @param {NodeJS.ProcessEnv} [options.env]
105
+ * @param {string} [options.taskId]
106
+ * @param {"task"|"always"|"off"} [options.mode]
107
+ * @returns {boolean}
108
+ */
109
+ export function shouldAddBosunCoAuthor(options = {}) {
110
+ return shouldAddBosunCoAuthorByContext(options);
111
+ }
@@ -26,9 +26,10 @@ export const LIBRARY_MANIFEST = "library.json";
26
26
  export const PROMPT_DIR = ".bosun/agents";
27
27
  export const SKILL_DIR = ".bosun/skills";
28
28
  export const PROFILE_DIR = ".bosun/profiles";
29
+ export const MCP_DIR = ".bosun/mcp-servers";
29
30
 
30
31
  /** Resource types managed by the library */
31
- export const RESOURCE_TYPES = Object.freeze(["prompt", "agent", "skill"]);
32
+ export const RESOURCE_TYPES = Object.freeze(["prompt", "agent", "skill", "mcp"]);
32
33
 
33
34
  // ── Helpers ───────────────────────────────────────────────────────────────────
34
35
 
@@ -140,12 +141,13 @@ function dirForType(rootDir, type) {
140
141
  case "prompt": return resolve(root, PROMPT_DIR);
141
142
  case "skill": return resolve(root, SKILL_DIR);
142
143
  case "agent": return resolve(root, PROFILE_DIR);
144
+ case "mcp": return resolve(root, MCP_DIR);
143
145
  default: throw new Error(`Unknown library resource type: ${type}`);
144
146
  }
145
147
  }
146
148
 
147
149
  function extForType(type) {
148
- return type === "agent" ? ".json" : ".md";
150
+ return (type === "agent" || type === "mcp") ? ".json" : ".md";
149
151
  }
150
152
 
151
153
  /**
@@ -439,9 +441,9 @@ export function detectScopes(repoRoot, opts = {}) {
439
441
  export function resolveLibraryRefs(template, rootDir, extraVars = {}) {
440
442
  if (typeof template !== "string") return "";
441
443
 
442
- // First resolve namespaced refs: {{prompt:name}}, {{agent:name}}, {{skill:name}}
444
+ // First resolve namespaced refs: {{prompt:name}}, {{agent:name}}, {{skill:name}}, {{mcp:name}}
443
445
  let resolved = template.replace(
444
- /\{\{\s*(prompt|agent|skill):([A-Za-z0-9_-]+)\s*\}\}/gi,
446
+ /\{\{\s*(prompt|agent|skill|mcp):([A-Za-z0-9_-]+)\s*\}\}/gi,
445
447
  (_full, type, name) => {
446
448
  const typeLower = type.toLowerCase();
447
449
  const id = slugify(name);
package/monitor.mjs CHANGED
@@ -98,7 +98,10 @@ import {
98
98
  resetMergeStrategyDedup,
99
99
  } from "./merge-strategy.mjs";
100
100
  import { assessTask, quickAssess } from "./task-assessment.mjs";
101
- import { getBosunCoAuthorTrailer } from "./git-commit-helpers.mjs";
101
+ import {
102
+ getBosunCoAuthorTrailer,
103
+ shouldAddBosunCoAuthor,
104
+ } from "./git-commit-helpers.mjs";
102
105
  import {
103
106
  normalizeDedupKey,
104
107
  stripAnsi,
@@ -1611,6 +1614,21 @@ try {
1611
1614
  let telegramNotifierInterval = null;
1612
1615
  let telegramNotifierTimeout = null;
1613
1616
  let weeklyReportLastSentAt = null;
1617
+ const monitorRestartReason = String(
1618
+ process.env.BOSUN_MONITOR_RESTART_REASON || "",
1619
+ )
1620
+ .trim()
1621
+ .toLowerCase();
1622
+
1623
+ function getTelegramBotStartOptions() {
1624
+ const restartReason = isSelfRestart
1625
+ ? "self-restart"
1626
+ : monitorRestartReason;
1627
+ return {
1628
+ restartReason,
1629
+ suppressPortalAutoOpen: restartReason.length > 0,
1630
+ };
1631
+ }
1614
1632
  let vkRecoveryLastAt = 0;
1615
1633
  let vkNonJsonNotifiedAt = 0;
1616
1634
  let vkNonJsonContentTypeLoggedAt = 0;
@@ -7259,15 +7277,15 @@ function extractPrNumberFromUrl(prUrl) {
7259
7277
  }
7260
7278
 
7261
7279
  function buildFlowGateMergeBody(taskTitle, taskId) {
7262
- const trailer = getBosunCoAuthorTrailer();
7263
7280
  const safeTitle = String(taskTitle || "Task").trim() || "Task";
7264
7281
  const safeId = String(taskId || "").trim();
7265
7282
  const lines = [
7266
7283
  `Merged by Bosun flow gate for: ${safeTitle}`,
7267
7284
  safeId ? `Task: ${safeId}` : "",
7268
- "",
7269
- trailer,
7270
7285
  ].filter(Boolean);
7286
+ if (shouldAddBosunCoAuthor({ taskId: safeId })) {
7287
+ lines.push("", getBosunCoAuthorTrailer());
7288
+ }
7271
7289
  return lines.join("\n");
7272
7290
  }
7273
7291
 
@@ -13725,7 +13743,7 @@ function applyConfig(nextConfig, options = {}) {
13725
13743
  }
13726
13744
  if (prevTelegramBotEnabled !== telegramBotEnabled) {
13727
13745
  if (telegramBotEnabled) {
13728
- void startTelegramBot();
13746
+ void startTelegramBot(getTelegramBotStartOptions());
13729
13747
  } else {
13730
13748
  stopTelegramBot();
13731
13749
  }
@@ -14139,17 +14157,16 @@ if (!isMonitorTestRuntime) {
14139
14157
  process.exit(0);
14140
14158
  }
14141
14159
 
14142
- // ── Codex CLI config.toml: ensure VK MCP + stream timeouts ──────────────────
14160
+ // ── Codex CLI config.toml: ensure global defaults + stream timeouts ─────────
14143
14161
  try {
14144
14162
  const vkPort = config.vkRecoveryPort || "54089";
14145
14163
  const vkBaseUrl = config.vkEndpointUrl || `http://127.0.0.1:${vkPort}`;
14146
- const skipVk = !isVkRuntimeRequired();
14147
14164
  const allowRuntimeCodexMutation = isTruthyFlag(
14148
14165
  process.env.BOSUN_ALLOW_RUNTIME_GLOBAL_CODEX_MUTATION,
14149
14166
  );
14150
14167
  const tomlResult = ensureCodexConfig({
14151
14168
  vkBaseUrl,
14152
- skipVk,
14169
+ skipVk: true,
14153
14170
  dryRun: !allowRuntimeCodexMutation,
14154
14171
  });
14155
14172
  if (!tomlResult.noChanges) {
@@ -15189,7 +15206,7 @@ injectMonitorFunctions({
15189
15206
  },
15190
15207
  });
15191
15208
  if (telegramBotEnabled) {
15192
- void startTelegramBot();
15209
+ void startTelegramBot(getTelegramBotStartOptions());
15193
15210
 
15194
15211
  // Process any commands queued by telegram-sentinel while monitor was down
15195
15212
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.34.6",
3
+ "version": "0.34.7",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -185,6 +185,7 @@
185
185
  "task-assessment.mjs",
186
186
  "task-complexity.mjs",
187
187
  "task-claims.mjs",
188
+ "task-context.mjs",
188
189
  "task-attachments.mjs",
189
190
  "task-executor.mjs",
190
191
  "task-store.mjs",