bosun 0.36.0 → 0.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.env.example +98 -16
  2. package/README.md +27 -0
  3. package/agent-event-bus.mjs +5 -5
  4. package/agent-pool.mjs +129 -12
  5. package/agent-prompts.mjs +7 -1
  6. package/agent-sdk.mjs +13 -2
  7. package/agent-supervisor.mjs +2 -2
  8. package/agent-work-report.mjs +1 -1
  9. package/anomaly-detector.mjs +6 -6
  10. package/autofix.mjs +15 -15
  11. package/bosun-skills.mjs +4 -4
  12. package/bosun.schema.json +160 -4
  13. package/claude-shell.mjs +11 -11
  14. package/cli.mjs +21 -21
  15. package/codex-config.mjs +19 -19
  16. package/codex-shell.mjs +180 -29
  17. package/config-doctor.mjs +27 -2
  18. package/config.mjs +60 -7
  19. package/copilot-shell.mjs +4 -4
  20. package/error-detector.mjs +1 -1
  21. package/fleet-coordinator.mjs +2 -2
  22. package/gemini-shell.mjs +692 -0
  23. package/github-oauth-portal.mjs +1 -1
  24. package/github-reconciler.mjs +2 -2
  25. package/kanban-adapter.mjs +741 -168
  26. package/merge-strategy.mjs +25 -25
  27. package/monitor.mjs +123 -105
  28. package/opencode-shell.mjs +22 -22
  29. package/package.json +7 -1
  30. package/postinstall.mjs +22 -22
  31. package/pr-cleanup-daemon.mjs +6 -6
  32. package/prepublish-check.mjs +4 -4
  33. package/presence.mjs +2 -2
  34. package/primary-agent.mjs +85 -7
  35. package/publish.mjs +1 -1
  36. package/review-agent.mjs +1 -1
  37. package/session-tracker.mjs +11 -0
  38. package/setup-web-server.mjs +429 -21
  39. package/setup.mjs +367 -12
  40. package/shared-knowledge.mjs +1 -1
  41. package/startup-service.mjs +9 -9
  42. package/stream-resilience.mjs +58 -4
  43. package/sync-engine.mjs +2 -2
  44. package/task-assessment.mjs +9 -9
  45. package/task-cli.mjs +1 -1
  46. package/task-complexity.mjs +71 -2
  47. package/task-context.mjs +1 -2
  48. package/task-executor.mjs +104 -41
  49. package/telegram-bot.mjs +825 -494
  50. package/telegram-sentinel.mjs +28 -28
  51. package/ui/app.js +256 -23
  52. package/ui/app.monolith.js +1 -1
  53. package/ui/components/agent-selector.js +4 -3
  54. package/ui/components/chat-view.js +101 -28
  55. package/ui/components/diff-viewer.js +3 -3
  56. package/ui/components/kanban-board.js +3 -3
  57. package/ui/components/session-list.js +255 -35
  58. package/ui/components/workspace-switcher.js +3 -3
  59. package/ui/demo.html +209 -194
  60. package/ui/index.html +3 -3
  61. package/ui/modules/icon-utils.js +206 -142
  62. package/ui/modules/icons.js +2 -27
  63. package/ui/modules/settings-schema.js +29 -5
  64. package/ui/modules/streaming.js +30 -2
  65. package/ui/modules/vision-stream.js +275 -0
  66. package/ui/modules/voice-client.js +102 -9
  67. package/ui/modules/voice-fallback.js +62 -6
  68. package/ui/modules/voice-overlay.js +594 -59
  69. package/ui/modules/voice.js +31 -38
  70. package/ui/setup.html +284 -34
  71. package/ui/styles/components.css +47 -0
  72. package/ui/styles/sessions.css +75 -0
  73. package/ui/tabs/agents.js +73 -43
  74. package/ui/tabs/chat.js +37 -40
  75. package/ui/tabs/control.js +2 -2
  76. package/ui/tabs/dashboard.js +1 -1
  77. package/ui/tabs/infra.js +10 -10
  78. package/ui/tabs/library.js +8 -8
  79. package/ui/tabs/logs.js +10 -10
  80. package/ui/tabs/settings.js +20 -20
  81. package/ui/tabs/tasks.js +76 -47
  82. package/ui-server.mjs +1761 -124
  83. package/update-check.mjs +13 -13
  84. package/ve-kanban.mjs +1 -1
  85. package/whatsapp-channel.mjs +5 -5
  86. package/workflow-engine.mjs +20 -1
  87. package/workflow-nodes.mjs +904 -4
  88. package/workflow-templates/agents.mjs +321 -7
  89. package/workflow-templates/ci-cd.mjs +6 -6
  90. package/workflow-templates/github.mjs +156 -84
  91. package/workflow-templates/planning.mjs +8 -8
  92. package/workflow-templates/reliability.mjs +8 -8
  93. package/workflow-templates/security.mjs +3 -3
  94. package/workflow-templates.mjs +15 -9
  95. package/workspace-manager.mjs +85 -1
  96. package/workspace-monitor.mjs +2 -2
  97. package/workspace-registry.mjs +2 -2
  98. package/worktree-manager.mjs +1 -1
@@ -328,7 +328,7 @@ function formatOpencodeEvent(event) {
328
328
  return null; // internal bookkeeping
329
329
 
330
330
  case "session.error":
331
- return `❌ OpenCode error: ${p.error || p.message || "unknown"}`;
331
+ return `:close: OpenCode error: ${p.error || p.message || "unknown"}`;
332
332
 
333
333
  // ── Message streaming ──────────────────────────────────────────────────
334
334
  case "message.part": {
@@ -338,7 +338,7 @@ function formatOpencodeEvent(event) {
338
338
  }
339
339
  // Reasoning / thinking blocks
340
340
  if (p.type === "thinking" && p.thinking) {
341
- return `💭 ${p.thinking.slice(0, 300)}`;
341
+ return `:u1f4ad: ${p.thinking.slice(0, 300)}`;
342
342
  }
343
343
  return null;
344
344
  }
@@ -360,31 +360,31 @@ function formatOpencodeEvent(event) {
360
360
  const tool = p.tool || "";
361
361
  if (tool.startsWith("mcp_")) {
362
362
  const [, server, ...nameParts] = tool.split("_");
363
- return `🔌 MCP [${server}]: ${nameParts.join("_")}`;
363
+ return `:plug: MCP [${server}]: ${nameParts.join("_")}`;
364
364
  }
365
365
  if (tool === "bash" || tool === "shell" || tool === "run") {
366
- return `⚡ Running: \`${p.input?.command || p.input?.cmd || tool}\``;
366
+ return `:zap: Running: \`${p.input?.command || p.input?.cmd || tool}\``;
367
367
  }
368
368
  if (tool === "write" || tool === "edit" || tool === "file_write") {
369
- return `✏️ Writing: ${p.input?.path || p.input?.file_path || "file"}`;
369
+ return `:edit: Writing: ${p.input?.path || p.input?.file_path || "file"}`;
370
370
  }
371
371
  if (tool === "read" || tool === "file_read") {
372
- return `📖 Reading: ${p.input?.path || p.input?.file_path || "file"}`;
372
+ return `:file: Reading: ${p.input?.path || p.input?.file_path || "file"}`;
373
373
  }
374
374
  if (tool === "web_search" || tool === "webSearch") {
375
- return `🔍 Searching: ${p.input?.query || ""}`;
375
+ return `:search: Searching: ${p.input?.query || ""}`;
376
376
  }
377
377
  if (tool === "glob" || tool === "find") {
378
- return `🔎 Finding: ${p.input?.pattern || p.input?.query || ""}`;
378
+ return `:search: Finding: ${p.input?.pattern || p.input?.query || ""}`;
379
379
  }
380
380
  // Generic tool
381
- return `🔧 Tool: ${tool}`;
381
+ return `:settings: Tool: ${tool}`;
382
382
  }
383
383
 
384
384
  case "tool.complete": {
385
385
  const tool = p.tool || "";
386
386
  const isError = !!p.error || p.exitCode !== undefined && p.exitCode !== 0;
387
- const status = isError ? "" : "";
387
+ const status = isError ? ":close:" : ":check:";
388
388
 
389
389
  if (tool.startsWith("mcp_")) {
390
390
  const [, server, ...nameParts] = tool.split("_");
@@ -407,12 +407,12 @@ function formatOpencodeEvent(event) {
407
407
  // ── File changes ───────────────────────────────────────────────────────
408
408
  case "file.updated":
409
409
  case "file.created": {
410
- const action = type === "file.created" ? "" : "✏️";
410
+ const action = type === "file.created" ? ":plus:" : ":edit:";
411
411
  return `${action} ${p.path || p.file || "file"}`;
412
412
  }
413
413
 
414
414
  case "file.deleted":
415
- return `🗑️ Deleted: ${p.path || p.file || "file"}`;
415
+ return `:trash: Deleted: ${p.path || p.file || "file"}`;
416
416
 
417
417
  // ── Error / completion ─────────────────────────────────────────────────
418
418
  case "prompt.completed":
@@ -421,7 +421,7 @@ function formatOpencodeEvent(event) {
421
421
 
422
422
  case "error":
423
423
  case "prompt.error":
424
- return `❌ Error: ${p.message || p.error || "unknown"}`;
424
+ return `:close: Error: ${p.message || p.error || "unknown"}`;
425
425
 
426
426
  default:
427
427
  return null;
@@ -485,7 +485,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
485
485
  agentSdk = resolveAgentSdkConfig({ reload: true });
486
486
  if (agentSdk.primary !== "opencode") {
487
487
  return {
488
- finalResponse: `❌ Agent SDK set to "${agentSdk.primary}" — OpenCode disabled.`,
488
+ finalResponse: `:close: Agent SDK set to "${agentSdk.primary}" — OpenCode disabled.`,
489
489
  items: [],
490
490
  usage: null,
491
491
  };
@@ -493,7 +493,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
493
493
 
494
494
  if (envFlagEnabled(process.env.OPENCODE_SDK_DISABLED)) {
495
495
  return {
496
- finalResponse: " OpenCode disabled via OPENCODE_SDK_DISABLED.",
496
+ finalResponse: ":close: OpenCode disabled via OPENCODE_SDK_DISABLED.",
497
497
  items: [],
498
498
  usage: null,
499
499
  };
@@ -501,7 +501,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
501
501
 
502
502
  if (activeTurn) {
503
503
  return {
504
- finalResponse: " OpenCode agent is still executing a previous task. Please wait.",
504
+ finalResponse: ":clock: OpenCode agent is still executing a previous task. Please wait.",
505
505
  items: [],
506
506
  usage: null,
507
507
  };
@@ -513,7 +513,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
513
513
  const started = await ensureServerStarted();
514
514
  if (!started) {
515
515
  return {
516
- finalResponse: " OpenCode server could not be started. Check that the opencode binary is on PATH.",
516
+ finalResponse: ":close: OpenCode server could not be started. Check that the opencode binary is on PATH.",
517
517
  items: [],
518
518
  usage: null,
519
519
  };
@@ -534,7 +534,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
534
534
  serverSessionId = await getOrCreateServerSession(namedId);
535
535
  } catch (err) {
536
536
  return {
537
- finalResponse: `❌ Could not establish OpenCode session: ${err.message}`,
537
+ finalResponse: `:close: Could not establish OpenCode session: ${err.message}`,
538
538
  items: [],
539
539
  usage: null,
540
540
  };
@@ -700,8 +700,8 @@ export async function execOpencodePrompt(userMessage, options = {}) {
700
700
  const reason = controller.signal.reason;
701
701
  const msg =
702
702
  reason === "user_stop"
703
- ? "🛑 Agent stopped by user."
704
- : `⏱️ Agent timed out after ${timeoutMs / 1000}s`;
703
+ ? ":close: Agent stopped by user."
704
+ : `:clock: Agent timed out after ${timeoutMs / 1000}s`;
705
705
 
706
706
  // Try to abort the server-side turn
707
707
  try {
@@ -725,7 +725,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
725
725
  continue;
726
726
  }
727
727
  return {
728
- finalResponse: `❌ OpenCode: connection failed after ${MAX_STREAM_RETRIES} retries: ${err.message}`,
728
+ finalResponse: `:close: OpenCode: connection failed after ${MAX_STREAM_RETRIES} retries: ${err.message}`,
729
729
  items: [],
730
730
  usage: null,
731
731
  };
@@ -736,7 +736,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
736
736
  }
737
737
 
738
738
  return {
739
- finalResponse: " OpenCode agent failed after all retry attempts.",
739
+ finalResponse: ":close: OpenCode agent failed after all retry attempts.",
740
740
  items: [],
741
741
  usage: null,
742
742
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.36.0",
3
+ "version": "0.36.2",
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",
@@ -39,6 +39,7 @@
39
39
  "./codex-shell": "./codex-shell.mjs",
40
40
  "./copilot-shell": "./copilot-shell.mjs",
41
41
  "./claude-shell": "./claude-shell.mjs",
42
+ "./gemini-shell": "./gemini-shell.mjs",
42
43
  "./primary-agent": "./primary-agent.mjs",
43
44
  "./maintenance": "./maintenance.mjs",
44
45
  "./telegram-bot": "./telegram-bot.mjs",
@@ -92,10 +93,13 @@
92
93
  "desktop:install": "npm -C desktop install",
93
94
  "desktop:dist": "npm -C desktop run dist",
94
95
  "build": "node vendor-sync.mjs",
96
+ "build:docs": "node build-docs.mjs",
95
97
  "shared-workspaces": "node shared-workspace-cli.mjs",
96
98
  "syntax:check": "node -e \"const fs=require('fs'),path=require('path');const files=fs.readdirSync('.').filter(f=>f.endsWith('.mjs'));let fail=0;for(const f of files){try{require('child_process').execSync('node --check '+f,{stdio:'pipe'});}catch(e){console.error('Syntax error: '+f);console.error(e.stderr.toString());fail=1;}}if(fail)process.exit(1);console.log('Syntax OK: '+files.length+' files checked');\"",
97
99
  "pretest": "npm run syntax:check",
98
100
  "test": "vitest run --config vitest.config.mjs",
101
+ "test:voice-provider-smoke": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs",
102
+ "check:native-call-parity": "vitest run --config vitest.config.mjs tests/voice-provider-smoke.test.mjs tests/native-call-parity-checklist.test.mjs",
99
103
  "test:watch": "vitest",
100
104
  "preinstall": "node -e \"try{var r=require('child_process').execSync('npm ls -g codex-monitor --json --depth=0',{encoding:'utf8',stdio:['pipe','pipe','pipe']});var d=JSON.parse(r).dependencies;if(d&&d['codex-monitor']){console.log('\\n Removing old codex-monitor package...');require('child_process').execSync('npm uninstall -g codex-monitor',{stdio:'inherit',timeout:30000});console.log(' \\u2705 Migrated to bosun. codex-monitor aliases still work.\\n')}}catch(e){}\"",
101
105
  "postinstall": "node postinstall.mjs",
@@ -139,6 +143,7 @@
139
143
  "config-doctor.mjs",
140
144
  "conflict-resolver.mjs",
141
145
  "copilot-shell.mjs",
146
+ "gemini-shell.mjs",
142
147
  "diff-stats.mjs",
143
148
  "desktop/main.mjs",
144
149
  "desktop/launch.mjs",
@@ -241,6 +246,7 @@
241
246
  "dependencies": {
242
247
  "@anthropic-ai/claude-agent-sdk": "latest",
243
248
  "@github/copilot-sdk": "latest",
249
+ "@google/genai": "latest",
244
250
  "@openai/codex-sdk": "latest",
245
251
  "@opencode-ai/sdk": "latest",
246
252
  "@preact/signals": "1.3.1",
package/postinstall.mjs CHANGED
@@ -267,9 +267,9 @@ async function main() {
267
267
  // Node.js version check
268
268
  const nodeMajor = Number(process.versions.node.split(".")[0]);
269
269
  if (nodeMajor >= 18) {
270
- console.log(` Node.js ${process.versions.node}`);
270
+ console.log(` :check: Node.js ${process.versions.node}`);
271
271
  } else {
272
- console.log(` Node.js ${process.versions.node} — requires ≥ 18`);
272
+ console.log(` :close: Node.js ${process.versions.node} — requires ≥ 18`);
273
273
  hasErrors = true;
274
274
  }
275
275
 
@@ -277,9 +277,9 @@ async function main() {
277
277
  for (const dep of REQUIRED) {
278
278
  if (commandExists(dep.cmd)) {
279
279
  const ver = getVersion(dep.cmd);
280
- console.log(` ${dep.name}${ver ? ` (${ver})` : ""}`);
280
+ console.log(` :check: ${dep.name}${ver ? ` (${ver})` : ""}`);
281
281
  } else {
282
- console.log(` ${dep.name} — REQUIRED`);
282
+ console.log(` :close: ${dep.name} — REQUIRED`);
283
283
  const hint = dep.install[platform] || dep.install.linux;
284
284
  console.log(` Install: ${hint}`);
285
285
  console.log(` Docs: ${dep.url}`);
@@ -301,9 +301,9 @@ async function main() {
301
301
  console.log(" ▸ Installing PowerShell (bundled)...");
302
302
  try {
303
303
  const info = await installBundledPwsh(platform, process.arch);
304
- console.log(` PowerShell bundled (${info.version})`);
304
+ console.log(` :check: PowerShell bundled (${info.version})`);
305
305
  } catch (err) {
306
- console.log(` ⚠️ PowerShell bundle install failed: ${err.message}`);
306
+ console.log(` :alert: PowerShell bundle install failed: ${err.message}`);
307
307
  }
308
308
  }
309
309
 
@@ -316,12 +316,12 @@ async function main() {
316
316
  if (hasPwsh) {
317
317
  const ver = getVersion(dep.cmd);
318
318
  if (isPwsh && bundledPwshExists() && !ver) {
319
- console.log(` ${dep.name} (bundled)`);
319
+ console.log(` :check: ${dep.name} (bundled)`);
320
320
  } else {
321
- console.log(` ${dep.name}${ver ? ` (${ver})` : ""}`);
321
+ console.log(` :check: ${dep.name}${ver ? ` (${ver})` : ""}`);
322
322
  }
323
323
  } else {
324
- console.log(` ⚠️ ${dep.name} — not found`);
324
+ console.log(` :alert: ${dep.name} — not found`);
325
325
  console.log(` ${dep.why}`);
326
326
  const hint = dep.install[platform] || dep.install.linux;
327
327
  console.log(` Install: ${hint}`);
@@ -330,12 +330,12 @@ async function main() {
330
330
  }
331
331
 
332
332
  // npm-installed tools (bundled with this package)
333
- console.log(` vibe-kanban (bundled)`);
334
- console.log(` @openai/codex-sdk (bundled)`);
335
- console.log(` @github/copilot-sdk (bundled)`);
336
- console.log(` @anthropic-ai/claude-agent-sdk (bundled)`);
337
- console.log(` @github/copilot-sdk (bundled)`);
338
- console.log(` @anthropic-ai/claude-agent-sdk (bundled)`);
333
+ console.log(` :check: vibe-kanban (bundled)`);
334
+ console.log(` :check: @openai/codex-sdk (bundled)`);
335
+ console.log(` :check: @github/copilot-sdk (bundled)`);
336
+ console.log(` :check: @anthropic-ai/claude-agent-sdk (bundled)`);
337
+ console.log(` :check: @github/copilot-sdk (bundled)`);
338
+ console.log(` :check: @anthropic-ai/claude-agent-sdk (bundled)`);
339
339
 
340
340
  // Desktop dependencies (Electron) — optional but recommended for instant launch
341
341
  const desktopDir = resolve(__dirname, "desktop");
@@ -355,10 +355,10 @@ async function main() {
355
355
  stdio: "inherit",
356
356
  timeout: 0,
357
357
  });
358
- console.log(" Desktop dependencies installed");
358
+ console.log(" :check: Desktop dependencies installed");
359
359
  } catch (err) {
360
360
  console.log(
361
- " ⚠️ Desktop dependency install failed — run manually:",
361
+ " :alert: Desktop dependency install failed — run manually:",
362
362
  );
363
363
  console.log(" npm -C scripts/bosun/desktop install");
364
364
  }
@@ -369,7 +369,7 @@ async function main() {
369
369
  console.log("");
370
370
  if (hasErrors) {
371
371
  console.log(
372
- " Missing required dependencies. Install them before running bosun.",
372
+ " :ban: Missing required dependencies. Install them before running bosun.",
373
373
  );
374
374
  } else if (hasWarnings) {
375
375
  console.log(
@@ -407,17 +407,17 @@ async function main() {
407
407
  const { ok, results } = await syncVendorFiles({ silent: true });
408
408
  const synced = results.filter((r) => r.source).length;
409
409
  if (ok) {
410
- console.log(` Vendor files bundled into ui/vendor/ (${synced}/${results.length} files)`);
410
+ console.log(` :check: Vendor files bundled into ui/vendor/ (${synced}/${results.length} files)`);
411
411
  } else {
412
412
  const missing = results.filter((r) => !r.source).map((r) => r.name);
413
- console.warn(` ⚠️ Some vendor files could not be bundled: ${missing.join(", ")}`);
413
+ console.warn(` :alert: Some vendor files could not be bundled: ${missing.join(", ")}`);
414
414
  console.warn(" The UI server will fall back to CDN for those files.");
415
415
  }
416
416
  } catch (err) {
417
- console.warn(` ⚠️ vendor-sync skipped: ${err.message}`);
417
+ console.warn(` :alert: vendor-sync skipped: ${err.message}`);
418
418
  }
419
419
  }
420
420
 
421
421
  main().catch((err) => {
422
- console.error(` ⚠️ postinstall failed: ${err.message}`);
422
+ console.error(` :alert: postinstall failed: ${err.message}`);
423
423
  });
@@ -326,7 +326,7 @@ class PRCleanupDaemon {
326
326
  if (verified.mergeable === "MERGEABLE") {
327
327
  this.stats.conflictsResolved++;
328
328
  console.log(
329
- `[pr-cleanup-daemon] Verified conflict resolution on PR #${pr.number} (mergeable=${verified.mergeable})`,
329
+ `[pr-cleanup-daemon] :check: Verified conflict resolution on PR #${pr.number} (mergeable=${verified.mergeable})`,
330
330
  );
331
331
  return true;
332
332
  }
@@ -347,7 +347,7 @@ class PRCleanupDaemon {
347
347
  if (verifiedLocal.mergeable === "MERGEABLE") {
348
348
  this.stats.conflictsResolved++;
349
349
  console.log(
350
- `[pr-cleanup-daemon] Verified conflict resolution on PR #${pr.number} after local fallback`,
350
+ `[pr-cleanup-daemon] :check: Verified conflict resolution on PR #${pr.number} after local fallback`,
351
351
  );
352
352
  return true;
353
353
  }
@@ -781,7 +781,7 @@ class PRCleanupDaemon {
781
781
  { cwd: this.repoRoot },
782
782
  );
783
783
  console.log(
784
- `[pr-cleanup-daemon] Auto-merge queued for PR #${pr.number} (CI pending)`,
784
+ `[pr-cleanup-daemon] :clock: Auto-merge queued for PR #${pr.number} (CI pending)`,
785
785
  );
786
786
  } catch {
787
787
  /* auto-merge may not be available */
@@ -802,7 +802,7 @@ class PRCleanupDaemon {
802
802
  await exec(`gh pr merge ${pr.number} --squash --delete-branch`, { cwd: this.repoRoot });
803
803
  this.stats.autoMerges++;
804
804
  console.log(
805
- `[pr-cleanup-daemon] Auto-merged green PR #${pr.number}: ${pr.title}`,
805
+ `[pr-cleanup-daemon] :check: Auto-merged green PR #${pr.number}: ${pr.title}`,
806
806
  );
807
807
  } catch (err) {
808
808
  // Fallback: enable auto-merge
@@ -812,7 +812,7 @@ class PRCleanupDaemon {
812
812
  { cwd: this.repoRoot },
813
813
  );
814
814
  console.log(
815
- `[pr-cleanup-daemon] Auto-merge enabled for PR #${pr.number}`,
815
+ `[pr-cleanup-daemon] :clock: Auto-merge enabled for PR #${pr.number}`,
816
816
  );
817
817
  } catch {
818
818
  console.warn(
@@ -836,7 +836,7 @@ class PRCleanupDaemon {
836
836
  */
837
837
  async escalate(pr, reason, context = {}) {
838
838
  const message =
839
- `⚠️ PR #${pr.number} escalated: ${reason}\n\n` +
839
+ `:alert: PR #${pr.number} escalated: ${reason}\n\n` +
840
840
  `Title: ${pr.title}\n` +
841
841
  `Context: ${JSON.stringify(context, null, 2)}\n\n` +
842
842
  `Manual intervention required.`;
@@ -21,7 +21,7 @@ const pkg = JSON.parse(
21
21
  );
22
22
 
23
23
  if (!pkg.version) {
24
- console.error(" Missing version in package.json");
24
+ console.error(":close: Missing version in package.json");
25
25
  process.exit(1);
26
26
  }
27
27
 
@@ -37,7 +37,7 @@ for (const f of filesArray) {
37
37
  }
38
38
  if (duplicates.length > 0) {
39
39
  console.error(
40
- `❌ Duplicate entries in files array: ${duplicates.join(", ")}`,
40
+ `:close: Duplicate entries in files array: ${duplicates.join(", ")}`,
41
41
  );
42
42
  process.exit(1);
43
43
  }
@@ -77,7 +77,7 @@ for (const file of mjsFiles) {
77
77
  }
78
78
 
79
79
  if (missing.length > 0) {
80
- console.error(" Local imports not in package.json files array:");
80
+ console.error(":close: Local imports not in package.json files array:");
81
81
  for (const { file, imported } of missing) {
82
82
  console.error(` ${file} → import from "./${imported}"`);
83
83
  }
@@ -86,5 +86,5 @@ if (missing.length > 0) {
86
86
  }
87
87
 
88
88
  console.log(
89
- `✅ ${pkg.name}@${pkg.version} — ${filesArray.length} files, ${mjsFiles.length} .mjs scanned, 0 missing imports`,
89
+ `:check: ${pkg.name}@${pkg.version} — ${filesArray.length} files, ${mjsFiles.length} .mjs scanned, 0 missing imports`,
90
90
  );
package/presence.mjs CHANGED
@@ -286,7 +286,7 @@ export function formatPresenceSummary({ nowMs, ttlMs } = {}) {
286
286
  return "No active instances reported.";
287
287
  }
288
288
  const coordinator = selectCoordinator({ nowMs, ttlMs });
289
- const lines = ["🛰️ Bosun Presence"];
289
+ const lines = [":server: Bosun Presence"];
290
290
  for (const entry of active) {
291
291
  const name = entry.instance_label || entry.instance_id;
292
292
  const role = entry.workspace_role || "workspace";
@@ -311,7 +311,7 @@ export function formatCoordinatorSummary({ nowMs, ttlMs } = {}) {
311
311
  const host = coordinator.host || "unknown";
312
312
  const lastSeen = coordinator.last_seen_at || coordinator.updated_at || "unknown";
313
313
  return [
314
- " Coordinator",
314
+ ":star: Coordinator",
315
315
  `Instance: ${name}`,
316
316
  `Role: ${role}`,
317
317
  `Host: ${host}`,
package/primary-agent.mjs CHANGED
@@ -51,6 +51,18 @@ import {
51
51
  switchSession as switchOpencodeSession,
52
52
  createSession as createOpencodeSession,
53
53
  } from "./opencode-shell.mjs";
54
+ import {
55
+ execGeminiPrompt,
56
+ steerGeminiPrompt,
57
+ isGeminiBusy,
58
+ getSessionInfo as getGeminiSessionInfo,
59
+ resetSession as resetGeminiSession,
60
+ initGeminiShell,
61
+ getActiveSessionId as getGeminiSessionId,
62
+ listSessions as listGeminiSessions,
63
+ switchSession as switchGeminiSession,
64
+ createSession as createGeminiSession,
65
+ } from "./gemini-shell.mjs";
54
66
  import { getModelsForExecutor, normalizeExecutorKey } from "./task-complexity.mjs";
55
67
 
56
68
  /** Valid agent interaction modes */
@@ -191,6 +203,31 @@ const ADAPTERS = {
191
203
  return execClaudePrompt(fullCmd, {});
192
204
  },
193
205
  },
206
+ "gemini-sdk": {
207
+ name: "gemini-sdk",
208
+ provider: "GEMINI",
209
+ displayName: "Gemini",
210
+ exec: (msg, opts) => execGeminiPrompt(msg, { persistent: true, ...opts }),
211
+ steer: steerGeminiPrompt,
212
+ isBusy: isGeminiBusy,
213
+ getInfo: () => getGeminiSessionInfo(),
214
+ reset: resetGeminiSession,
215
+ init: async () => initGeminiShell(),
216
+ getSessionId: getGeminiSessionId,
217
+ listSessions: listGeminiSessions,
218
+ switchSession: switchGeminiSession,
219
+ createSession: createGeminiSession,
220
+ sdkCommands: ["/status", "/model", "/clear"],
221
+ execSdkCommand: async (command, args) => {
222
+ const cmd = command.startsWith("/") ? command : `/${command}`;
223
+ if (cmd === "/clear") {
224
+ await resetGeminiSession();
225
+ return "Session cleared.";
226
+ }
227
+ const fullCmd = args ? `${cmd} ${args}` : cmd;
228
+ return execGeminiPrompt(fullCmd, { persistent: true });
229
+ },
230
+ },
194
231
  "opencode-sdk": {
195
232
  name: "opencode-sdk",
196
233
  provider: "OPENCODE",
@@ -336,6 +373,8 @@ function normalizePrimaryAgent(value) {
336
373
  return "copilot-sdk";
337
374
  if (["claude", "claude-sdk", "claude_code", "claude-code"].includes(raw))
338
375
  return "claude-sdk";
376
+ if (["gemini", "gemini-sdk", "google-gemini"].includes(raw))
377
+ return "gemini-sdk";
339
378
  if (["opencode", "opencode-sdk", "open-code"].includes(raw))
340
379
  return "opencode-sdk";
341
380
  return raw;
@@ -354,6 +393,7 @@ function executorToAdapter(executor) {
354
393
  const key = normalizeExecutorKey(executor);
355
394
  if (key === "copilot") return "copilot-sdk";
356
395
  if (key === "claude") return "claude-sdk";
396
+ if (key === "gemini") return "gemini-sdk";
357
397
  if (key === "opencode") return "opencode-sdk";
358
398
  return "codex-sdk";
359
399
  }
@@ -474,6 +514,10 @@ export async function initPrimaryAgent(nameOrConfig = null) {
474
514
  setPrimaryAgent("copilot-sdk");
475
515
  } else if (!envFlagEnabled(process.env.CLAUDE_SDK_DISABLED)) {
476
516
  setPrimaryAgent("claude-sdk");
517
+ } else if (!envFlagEnabled(process.env.GEMINI_SDK_DISABLED)) {
518
+ setPrimaryAgent("gemini-sdk");
519
+ } else if (!envFlagEnabled(process.env.OPENCODE_SDK_DISABLED)) {
520
+ setPrimaryAgent("opencode-sdk");
477
521
  }
478
522
  }
479
523
 
@@ -485,6 +529,22 @@ export async function initPrimaryAgent(nameOrConfig = null) {
485
529
  setPrimaryAgent("codex-sdk");
486
530
  }
487
531
 
532
+ if (
533
+ activeAdapter.name === "gemini-sdk" &&
534
+ envFlagEnabled(process.env.GEMINI_SDK_DISABLED)
535
+ ) {
536
+ primaryFallbackReason = "Gemini SDK disabled — falling back to Codex";
537
+ setPrimaryAgent("codex-sdk");
538
+ }
539
+
540
+ if (
541
+ activeAdapter.name === "opencode-sdk" &&
542
+ envFlagEnabled(process.env.OPENCODE_SDK_DISABLED)
543
+ ) {
544
+ primaryFallbackReason = "OpenCode SDK disabled — falling back to Codex";
545
+ setPrimaryAgent("codex-sdk");
546
+ }
547
+
488
548
  ensurePrimaryAgentConfigs(activeAdapter.name);
489
549
 
490
550
  const ok = await activeAdapter.init();
@@ -494,6 +554,18 @@ export async function initPrimaryAgent(nameOrConfig = null) {
494
554
  ensurePrimaryAgentConfigs(activeAdapter.name);
495
555
  await activeAdapter.init();
496
556
  }
557
+ if (activeAdapter.name === "gemini-sdk" && ok === false) {
558
+ primaryFallbackReason = "Gemini SDK unavailable — falling back to Codex";
559
+ setPrimaryAgent("codex-sdk");
560
+ ensurePrimaryAgentConfigs(activeAdapter.name);
561
+ await activeAdapter.init();
562
+ }
563
+ if (activeAdapter.name === "opencode-sdk" && ok === false) {
564
+ primaryFallbackReason = "OpenCode SDK unavailable — falling back to Codex";
565
+ setPrimaryAgent("codex-sdk");
566
+ ensurePrimaryAgentConfigs(activeAdapter.name);
567
+ await activeAdapter.init();
568
+ }
497
569
 
498
570
  initialized = true;
499
571
  return getPrimaryAgentName();
@@ -506,7 +578,13 @@ const PRIMARY_EXEC_TIMEOUT_MS = Number(process.env.PRIMARY_AGENT_TIMEOUT_MS) ||
506
578
  const MAX_FAILOVER_ATTEMPTS = 2;
507
579
 
508
580
  /** Ordered fallback chain — if the current adapter times out, try the next */
509
- const FALLBACK_ORDER = ["codex-sdk", "copilot-sdk", "claude-sdk"];
581
+ const FALLBACK_ORDER = [
582
+ "codex-sdk",
583
+ "copilot-sdk",
584
+ "claude-sdk",
585
+ "gemini-sdk",
586
+ "opencode-sdk",
587
+ ];
510
588
 
511
589
  function mapAdapterToPoolSdk(adapterName) {
512
590
  const normalized = String(adapterName || "").trim().toLowerCase();
@@ -632,12 +710,12 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
632
710
  // If failing over to a different adapter, switch and init
633
711
  if (attempt > 0) {
634
712
  console.warn(
635
- `[primary-agent] ⚠️ Failing over from ${adaptersToTry[attempt - 1]} to ${adapterName} (reason: ${lastError?.message || "unknown"})`,
713
+ `[primary-agent] :alert: Failing over from ${adaptersToTry[attempt - 1]} to ${adapterName} (reason: ${lastError?.message || "unknown"})`,
636
714
  );
637
715
  tracker.recordEvent(sessionId, {
638
716
  role: "system",
639
717
  type: "failover",
640
- content: `⚠️ Agent "${adaptersToTry[attempt - 1]}" failed — switching to "${adapterName}": ${lastError?.message || "timeout/error"}`,
718
+ content: `:alert: Agent "${adaptersToTry[attempt - 1]}" failed — switching to "${adapterName}": ${lastError?.message || "timeout/error"}`,
641
719
  timestamp: new Date().toISOString(),
642
720
  });
643
721
  setPrimaryAgent(adapterName);
@@ -691,7 +769,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
691
769
  lastError = err;
692
770
  const isTimeout = err.message?.startsWith("AGENT_TIMEOUT");
693
771
  console.error(
694
- `[primary-agent] ${isTimeout ? "⏱️ Timeout" : " Error"} with ${adapterName}: ${err.message}`,
772
+ `[primary-agent] ${isTimeout ? ":clock: Timeout" : ":close: Error"} with ${adapterName}: ${err.message}`,
695
773
  );
696
774
 
697
775
  // If this is the last adapter, report to user
@@ -700,8 +778,8 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
700
778
  role: "system",
701
779
  type: "error",
702
780
  content: isTimeout
703
- ? `⏱️ All agents timed out. The AI service may be experiencing issues. Your message was saved — please try again shortly.`
704
- : `❌ Agent error: ${err.message}. Your message was saved — please try again.`,
781
+ ? `:clock: All agents timed out. The AI service may be experiencing issues. Your message was saved — please try again shortly.`
782
+ : `:close: Agent error: ${err.message}. Your message was saved — please try again.`,
705
783
  timestamp: new Date().toISOString(),
706
784
  });
707
785
  }
@@ -710,7 +788,7 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
710
788
 
711
789
  // All adapters failed
712
790
  return {
713
- finalResponse: `❌ All agent adapters failed. Last error: ${lastError?.message || "unknown"}`,
791
+ finalResponse: `:close: All agent adapters failed. Last error: ${lastError?.message || "unknown"}`,
714
792
  items: [],
715
793
  usage: null,
716
794
  };
package/publish.mjs CHANGED
@@ -225,7 +225,7 @@ function main() {
225
225
  if (status === 0 && !dryRun) {
226
226
  console.log(
227
227
  "\n[publish] :\n" +
228
- " npm deprecate openfleet@'*' \"⚠️ openfleet has been renamed to bosun. Install the latest: npm install -g bosun\"\n",
228
+ " npm deprecate openfleet@'*' \":alert: openfleet has been renamed to bosun. Install the latest: npm install -g bosun\"\n",
229
229
  );
230
230
  }
231
231
  process.exit(status);
package/review-agent.mjs CHANGED
@@ -588,7 +588,7 @@ export class ReviewAgent {
588
588
  .join("\n");
589
589
 
590
590
  const message = [
591
- `🔍 Review: changes requested`,
591
+ `:search: Review: changes requested`,
592
592
  `Task: ${taskId}`,
593
593
  `Summary: ${result.summary}`,
594
594
  result.issues.length ? `\nIssues:\n${issueList}` : "",
@@ -1005,6 +1005,15 @@ ${items.join("\n")}` : "todo updated";
1005
1005
  };
1006
1006
  }
1007
1007
 
1008
+ if (event.type === "session.idle" || event.type === "session.completed") {
1009
+ return {
1010
+ type: "system",
1011
+ content: "Session completed",
1012
+ timestamp: ts,
1013
+ meta: { lifecycle: "session_completed" },
1014
+ };
1015
+ }
1016
+
1008
1017
  if (event.type === "turn.failed") {
1009
1018
  const detail = toText(event.error?.message || "unknown error");
1010
1019
  return {
@@ -1067,10 +1076,12 @@ ${items.join("\n")}` : "todo updated";
1067
1076
  }
1068
1077
 
1069
1078
  if (event.type === "message_stop" || event.type === "message_delta") {
1079
+ const lifecycle = event.type === "message_stop" ? "turn_completed" : undefined;
1070
1080
  return {
1071
1081
  type: "system",
1072
1082
  content: `${event.type}${event.delta?.stop_reason ? ` (${event.delta.stop_reason})` : ""}`,
1073
1083
  timestamp: ts,
1084
+ ...(lifecycle ? { meta: { lifecycle } } : {}),
1074
1085
  };
1075
1086
  }
1076
1087