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
package/autofix.mjs CHANGED
@@ -776,7 +776,7 @@ export async function attemptAutoFix(opts) {
776
776
  );
777
777
  if (onTelegram) {
778
778
  onTelegram(
779
- `🔧 Auto-fix gave up on raw crash (${reason}) after ${MAX_FIX_ATTEMPTS} attempts.\nManual intervention required.`,
779
+ `:settings: Auto-fix gave up on raw crash (${reason}) after ${MAX_FIX_ATTEMPTS} attempts.\nManual intervention required.`,
780
780
  );
781
781
  }
782
782
  return {
@@ -794,7 +794,7 @@ export async function attemptAutoFix(opts) {
794
794
 
795
795
  if (onTelegram) {
796
796
  onTelegram(
797
- `🔧 Auto-fix starting [${modeLabel}] (raw fallback, attempt #${attemptNum}):\nCrash: ${reason}\nError indicators: ${fallback.errorLines.length} suspicious lines`,
797
+ `:settings: Auto-fix starting [${modeLabel}] (raw fallback, attempt #${attemptNum}):\nCrash: ${reason}\nError indicators: ${fallback.errorLines.length} suspicious lines`,
798
798
  );
799
799
  }
800
800
 
@@ -830,7 +830,7 @@ export async function attemptAutoFix(opts) {
830
830
  console.log("[autofix] npm mode — skipping execution, sending analysis");
831
831
 
832
832
  const suggestion =
833
- `📋 *Auto-fix analysis* (raw fallback, attempt #${attemptNum}):\n` +
833
+ `:clipboard: *Auto-fix analysis* (raw fallback, attempt #${attemptNum}):\n` +
834
834
  `Crash: ${reason}\n\n` +
835
835
  `**Error indicators found:**\n` +
836
836
  (fallback.errorLines.length > 0
@@ -889,7 +889,7 @@ export async function attemptAutoFix(opts) {
889
889
 
890
890
  if (result.success && newChanges.length > 0) {
891
891
  const outcomeMsg =
892
- `🔧 Auto-fix applied (raw fallback, attempt #${attemptNum}):\n` +
892
+ `:settings: Auto-fix applied (raw fallback, attempt #${attemptNum}):\n` +
893
893
  `Crash: ${reason}\n` +
894
894
  `Changes:\n${changeSummary}\n` +
895
895
  `Codex SDK log: ${result.logPath}`;
@@ -904,7 +904,7 @@ export async function attemptAutoFix(opts) {
904
904
  };
905
905
  } else {
906
906
  const outcomeMsg =
907
- `🔧 Auto-fix fallback failed (attempt #${attemptNum}):\n` +
907
+ `:settings: Auto-fix fallback failed (attempt #${attemptNum}):\n` +
908
908
  `Crash: ${reason}\n` +
909
909
  `Codex: ${result.error || "no changes written"}\n` +
910
910
  `Codex SDK log: ${result.logPath}`;
@@ -931,7 +931,7 @@ export async function attemptAutoFix(opts) {
931
931
  .map((e) => `• ${e.errorType}: ${e.file}:${e.line}`)
932
932
  .join("\n");
933
933
  onTelegram(
934
- `🔧 Auto-fix starting [${modeLabel}]:\nFound ${errors.length} error(s):\n${errorSummary}`,
934
+ `:settings: Auto-fix starting [${modeLabel}]:\nFound ${errors.length} error(s):\n${errorSummary}`,
935
935
  );
936
936
  }
937
937
 
@@ -950,7 +950,7 @@ export async function attemptAutoFix(opts) {
950
950
 
951
951
  if (count >= MAX_FIX_ATTEMPTS && onTelegram) {
952
952
  onTelegram(
953
- `🔧 Auto-fix gave up on ${error.file}:${error.line} after ${MAX_FIX_ATTEMPTS} attempts.\n` +
953
+ `:settings: Auto-fix gave up on ${error.file}:${error.line} after ${MAX_FIX_ATTEMPTS} attempts.\n` +
954
954
  `Error: ${error.message}\nManual intervention required.`,
955
955
  );
956
956
  }
@@ -1004,7 +1004,7 @@ export async function attemptAutoFix(opts) {
1004
1004
  // ── NPM mode: analyze only, suggest fix to user ──────────────────
1005
1005
  if (!devMode) {
1006
1006
  const suggestion =
1007
- `📋 *Auto-fix analysis* (attempt #${attemptNum}):\n` +
1007
+ `:clipboard: *Auto-fix analysis* (attempt #${attemptNum}):\n` +
1008
1008
  `**${error.errorType}** at \`${error.file}:${error.line}\`\n` +
1009
1009
  `Message: ${error.message}\n` +
1010
1010
  (error.codeLine ? `Failing code: \`${error.codeLine}\`\n` : "") +
@@ -1058,7 +1058,7 @@ export async function attemptAutoFix(opts) {
1058
1058
 
1059
1059
  if (result.success) {
1060
1060
  const outcomeMsg =
1061
- `🔧 Auto-fix applied (attempt #${attemptNum}):\n` +
1061
+ `:settings: Auto-fix applied (attempt #${attemptNum}):\n` +
1062
1062
  `${error.errorType} at ${error.file}:${error.line}\n` +
1063
1063
  `"${error.message}"\n` +
1064
1064
  `Changes:\n${changeSummary}`;
@@ -1072,7 +1072,7 @@ export async function attemptAutoFix(opts) {
1072
1072
  if (onTelegram) onTelegram(outcomeMsg);
1073
1073
  } else {
1074
1074
  const outcomeMsg =
1075
- `🔧 Auto-fix failed (attempt #${attemptNum}):\n` +
1075
+ `:settings: Auto-fix failed (attempt #${attemptNum}):\n` +
1076
1076
  `${error.errorType} at ${error.file}:${error.line}\n` +
1077
1077
  `Codex: ${result.error || "no changes written"}`;
1078
1078
 
@@ -1235,7 +1235,7 @@ export async function fixLoopingError(opts) {
1235
1235
 
1236
1236
  if (!canAttemptFix(signature)) {
1237
1237
  const count = getFixAttemptCount(signature);
1238
- const outcome = `🔁 Loop fix gave up on repeating error after ${count} attempts.\n"${errorLine.slice(0, 200)}"\nManual intervention required.`;
1238
+ const outcome = `:repeat: Loop fix gave up on repeating error after ${count} attempts.\n"${errorLine.slice(0, 200)}"\nManual intervention required.`;
1239
1239
  console.warn(`[autofix] loop fix exhausted for: ${errorLine.slice(0, 80)}`);
1240
1240
  if (onTelegram) onTelegram(outcome);
1241
1241
  return { fixed: false, outcome };
@@ -1248,7 +1248,7 @@ export async function fixLoopingError(opts) {
1248
1248
 
1249
1249
  if (onTelegram) {
1250
1250
  onTelegram(
1251
- `🔁 Repeating error detected [${modeLabel}] (${repeatCount}x, fix attempt #${attemptNum}):\n"${errorLine.slice(0, 200)}"`,
1251
+ `:repeat: Repeating error detected [${modeLabel}] (${repeatCount}x, fix attempt #${attemptNum}):\n"${errorLine.slice(0, 200)}"`,
1252
1252
  );
1253
1253
  }
1254
1254
 
@@ -1312,7 +1312,7 @@ ${messagesCtx}
1312
1312
  console.log("[autofix] npm mode — loop fix: analysis only");
1313
1313
 
1314
1314
  const suggestion =
1315
- `📋 *Loop fix analysis* (attempt #${attemptNum}):\n` +
1315
+ `:clipboard: *Loop fix analysis* (attempt #${attemptNum}):\n` +
1316
1316
  `**Repeating error** (${repeatCount}x):\n` +
1317
1317
  `\`${errorLine.slice(0, 300)}\`\n\n` +
1318
1318
  `**Likely cause:** This error is repeating in a loop, likely because:\n` +
@@ -1359,7 +1359,7 @@ ${messagesCtx}
1359
1359
 
1360
1360
  if (result.success && newChanges.length > 0) {
1361
1361
  const outcome =
1362
- `🔁 Loop fix applied (attempt #${attemptNum}):\n` +
1362
+ `:repeat: Loop fix applied (attempt #${attemptNum}):\n` +
1363
1363
  `Error: "${errorLine.slice(0, 150)}"\n` +
1364
1364
  `Changes:\n${changeSummary}`;
1365
1365
  console.log(`[autofix] loop fix applied: ${newChanges.join(", ")}`);
@@ -1367,7 +1367,7 @@ ${messagesCtx}
1367
1367
  return { fixed: true, outcome };
1368
1368
  } else {
1369
1369
  const outcome =
1370
- `🔁 Loop fix failed (attempt #${attemptNum}):\n` +
1370
+ `:repeat: Loop fix failed (attempt #${attemptNum}):\n` +
1371
1371
  `Error: "${errorLine.slice(0, 150)}"\n` +
1372
1372
  `Codex: ${result.error || "no changes written"}`;
1373
1373
  console.warn(`[autofix] loop fix codex exec failed: ${result.error}`);
package/bosun-skills.mjs CHANGED
@@ -375,10 +375,10 @@ The scope is the **module or area** affected:
375
375
 
376
376
  ## Common Mistakes to Avoid
377
377
 
378
- - \`git commit -m "fix stuff"\` — too vague
379
- - \`git commit -m "WIP: not done yet"\` — commit only complete, testable units
380
- - \`git commit -am "…"\` — stages all tracked changes indiscriminately
381
- - \`git add src/auth/login.ts && git commit -m "fix(auth): handle empty token gracefully"\`
378
+ - :close: \`git commit -m "fix stuff"\` — too vague
379
+ - :close: \`git commit -m "WIP: not done yet"\` — commit only complete, testable units
380
+ - :close: \`git commit -am "…"\` — stages all tracked changes indiscriminately
381
+ - :check: \`git add src/auth/login.ts && git commit -m "fix(auth): handle empty token gracefully"\`
382
382
  `,
383
383
  },
384
384
  {
package/bosun.schema.json CHANGED
@@ -38,7 +38,98 @@
38
38
  "codexEnabled": { "type": "boolean" },
39
39
  "primaryAgent": {
40
40
  "type": "string",
41
- "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"]
41
+ "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"]
42
+ },
43
+ "telegramUiTunnel": {
44
+ "type": "string",
45
+ "enum": ["named", "quick", "auto", "cloudflared", "disabled"],
46
+ "default": "named",
47
+ "description": "Mini App tunnel mode. named is the default permanent hostname mode."
48
+ },
49
+ "telegramUiAllowQuickTunnelFallback": {
50
+ "type": "boolean",
51
+ "default": false,
52
+ "description": "Allow quick trycloudflare fallback only when named tunnel startup fails."
53
+ },
54
+ "cloudflareTunnelName": {
55
+ "type": "string",
56
+ "description": "Named Cloudflare tunnel identifier."
57
+ },
58
+ "cloudflareTunnelCredentials": {
59
+ "type": "string",
60
+ "description": "Path to cloudflared tunnel credentials JSON."
61
+ },
62
+ "cloudflareBaseDomain": {
63
+ "type": "string",
64
+ "description": "Base domain used for deterministic per-user tunnel hostnames."
65
+ },
66
+ "cloudflareTunnelHostname": {
67
+ "type": "string",
68
+ "description": "Explicit tunnel hostname override."
69
+ },
70
+ "cloudflareUsernameHostnamePolicy": {
71
+ "type": "string",
72
+ "enum": ["per-user-fixed", "fixed"],
73
+ "default": "per-user-fixed",
74
+ "description": "Policy for deriving named tunnel hostname from workstation identity."
75
+ },
76
+ "cloudflareZoneId": {
77
+ "type": "string",
78
+ "description": "Cloudflare zone ID used for DNS orchestration."
79
+ },
80
+ "cloudflareApiToken": {
81
+ "type": "string",
82
+ "description": "Cloudflare API token scoped to Zone DNS edit."
83
+ },
84
+ "cloudflareDnsSyncEnabled": {
85
+ "type": "boolean",
86
+ "default": true,
87
+ "description": "Enable Cloudflare DNS idempotent create/verify/update during named tunnel startup."
88
+ },
89
+ "cloudflareDnsMaxRetries": {
90
+ "type": "number",
91
+ "default": 3,
92
+ "description": "Retry attempts for Cloudflare DNS API operations."
93
+ },
94
+ "cloudflareDnsRetryBaseMs": {
95
+ "type": "number",
96
+ "default": 750,
97
+ "description": "Base backoff delay for Cloudflare DNS API retries."
98
+ },
99
+ "telegramUiFallbackAuthEnabled": {
100
+ "type": "boolean",
101
+ "default": true,
102
+ "description": "Enable admin fallback PIN/password auth when Telegram/session auth is unavailable."
103
+ },
104
+ "telegramUiFallbackAuthRateLimitIpPerMin": {
105
+ "type": "number",
106
+ "default": 10,
107
+ "description": "Per-IP fallback auth attempt rate limit per minute."
108
+ },
109
+ "telegramUiFallbackAuthRateLimitGlobalPerMin": {
110
+ "type": "number",
111
+ "default": 60,
112
+ "description": "Global fallback auth attempt rate limit per minute."
113
+ },
114
+ "telegramUiFallbackAuthMaxFailures": {
115
+ "type": "number",
116
+ "default": 5,
117
+ "description": "Failed fallback auth attempts before temporary lockout."
118
+ },
119
+ "telegramUiFallbackAuthLockoutMs": {
120
+ "type": "number",
121
+ "default": 600000,
122
+ "description": "Temporary lockout duration in milliseconds after max fallback auth failures."
123
+ },
124
+ "telegramUiFallbackAuthRotateDays": {
125
+ "type": "number",
126
+ "default": 30,
127
+ "description": "Target credential rotation interval for fallback auth."
128
+ },
129
+ "telegramUiFallbackAuthTransientCooldownMs": {
130
+ "type": "number",
131
+ "default": 5000,
132
+ "description": "Cooldown when fallback auth backend has transient errors."
42
133
  },
43
134
  "voice": {
44
135
  "type": "object",
@@ -52,15 +143,20 @@
52
143
  },
53
144
  "provider": {
54
145
  "type": "string",
55
- "enum": ["openai", "azure", "fallback", "auto"],
146
+ "enum": ["openai", "azure", "claude", "gemini", "fallback", "auto"],
56
147
  "default": "auto",
57
- "description": "Voice provider: openai (direct API), azure (Azure OpenAI), fallback (browser STT/TTS), auto (detect from env)"
148
+ "description": "Voice provider: openai/azure (Tier 1 realtime), claude/gemini (Tier 2 voice + provider vision), fallback (browser STT/TTS), auto (detect from env)"
58
149
  },
59
150
  "model": {
60
151
  "type": "string",
61
152
  "default": "gpt-4o-realtime-preview-2024-12-17",
62
153
  "description": "Realtime API model name"
63
154
  },
155
+ "visionModel": {
156
+ "type": "string",
157
+ "default": "gpt-4.1-mini",
158
+ "description": "Vision model used for live screen/camera frame analysis"
159
+ },
64
160
  "openaiApiKey": {
65
161
  "type": "string",
66
162
  "description": "OpenAI API key for Realtime API (overrides OPENAI_API_KEY env)"
@@ -78,6 +174,14 @@
78
174
  "default": "gpt-4o-realtime-preview",
79
175
  "description": "Azure OpenAI deployment name"
80
176
  },
177
+ "claudeApiKey": {
178
+ "type": "string",
179
+ "description": "Anthropic API key for Claude voice/vision provider mode"
180
+ },
181
+ "geminiApiKey": {
182
+ "type": "string",
183
+ "description": "Gemini API key for Gemini voice/vision provider mode"
184
+ },
81
185
  "voiceId": {
82
186
  "type": "string",
83
187
  "enum": ["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"],
@@ -102,7 +206,7 @@
102
206
  },
103
207
  "delegateExecutor": {
104
208
  "type": "string",
105
- "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"],
209
+ "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"],
106
210
  "description": "Which executor to use for delegate_to_agent calls. Defaults to primaryAgent."
107
211
  }
108
212
  }
@@ -456,6 +560,58 @@
456
560
  }
457
561
  ]
458
562
  },
563
+ "internalExecutor": {
564
+ "type": "object",
565
+ "additionalProperties": true,
566
+ "properties": {
567
+ "mode": {
568
+ "type": "string",
569
+ "enum": ["vk", "internal", "hybrid"]
570
+ },
571
+ "maxParallel": { "type": "number" },
572
+ "baseBranchParallelLimit": { "type": "number" },
573
+ "pollIntervalMs": { "type": "number" },
574
+ "sdk": {
575
+ "type": "string",
576
+ "enum": ["auto", "codex", "copilot", "claude", "gemini", "opencode"]
577
+ },
578
+ "taskTimeoutMs": { "type": "number" },
579
+ "maxRetries": { "type": "number" },
580
+ "reviewAgentEnabled": { "type": "boolean" },
581
+ "reviewMaxConcurrent": { "type": "number" },
582
+ "reviewTimeoutMs": { "type": "number" },
583
+ "stream": {
584
+ "type": "object",
585
+ "additionalProperties": false,
586
+ "properties": {
587
+ "maxRetries": {
588
+ "type": "number",
589
+ "description": "Transient stream retry attempts per run"
590
+ },
591
+ "retryBaseMs": {
592
+ "type": "number",
593
+ "description": "Base stream retry delay in milliseconds"
594
+ },
595
+ "retryMaxMs": {
596
+ "type": "number",
597
+ "description": "Maximum stream retry delay in milliseconds"
598
+ },
599
+ "firstEventTimeoutMs": {
600
+ "type": "number",
601
+ "description": "Abort/retry if no stream events arrive within this timeout"
602
+ },
603
+ "maxItemsPerTurn": {
604
+ "type": "number",
605
+ "description": "Maximum completed stream items retained per turn"
606
+ },
607
+ "maxItemChars": {
608
+ "type": "number",
609
+ "description": "Maximum characters retained per string field in stored stream items"
610
+ }
611
+ }
612
+ }
613
+ }
614
+ },
459
615
  "profiles": { "$ref": "#/$defs/profiles" },
460
616
  "envProfiles": { "$ref": "#/$defs/profiles" },
461
617
  "executors": {
package/claude-shell.mjs CHANGED
@@ -105,27 +105,27 @@ function formatEvent(event) {
105
105
  event.type === "item.started" &&
106
106
  event.item?.type === "command_execution"
107
107
  ) {
108
- return `⚡ Running: \`${event.item.command}\``;
108
+ return `:zap: Running: \`${event.item.command}\``;
109
109
  }
110
110
  if (
111
111
  event.type === "item.completed" &&
112
112
  event.item?.type === "command_execution"
113
113
  ) {
114
- const status = event.item.exit_code === 0 ? "" : "";
114
+ const status = event.item.exit_code === 0 ? ":check:" : ":close:";
115
115
  return `${status} Command done: \`${event.item.command}\``;
116
116
  }
117
117
  if (event.type === "item.started" && event.item?.type === "mcp_tool_call") {
118
- return `🔌 MCP [${event.item.server}/${event.item.tool}]`;
118
+ return `:plug: MCP [${event.item.server}/${event.item.tool}]`;
119
119
  }
120
120
  if (event.type === "item.completed" && event.item?.type === "mcp_tool_call") {
121
- const status = event.item.status === "completed" ? "" : "";
121
+ const status = event.item.status === "completed" ? ":check:" : ":close:";
122
122
  return `${status} MCP [${event.item.server}/${event.item.tool}]`;
123
123
  }
124
124
  if (event.type === "item.started" && event.item?.type === "web_search") {
125
- return `🔍 Searching: ${event.item.query || ""}`;
125
+ return `:search: Searching: ${event.item.query || ""}`;
126
126
  }
127
127
  if (event.type === "item.updated" && event.item?.type === "reasoning") {
128
- return event.item.text ? `💭 ${event.item.text.slice(0, 300)}` : null;
128
+ return event.item.text ? `:u1f4ad: ${event.item.text.slice(0, 300)}` : null;
129
129
  }
130
130
  return null;
131
131
  }
@@ -496,7 +496,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
496
496
  if (activeTurn && !options._holdActiveTurn) {
497
497
  return {
498
498
  finalResponse:
499
- " Agent is still executing a previous task. Please wait.",
499
+ ":clock: Agent is still executing a previous task. Please wait.",
500
500
  items: [],
501
501
  usage: null,
502
502
  };
@@ -505,7 +505,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
505
505
  const query = await loadClaudeSdk();
506
506
  if (!query) {
507
507
  return {
508
- finalResponse: " Claude SDK not available.",
508
+ finalResponse: ":close: Claude SDK not available.",
509
509
  items: [],
510
510
  usage: null,
511
511
  };
@@ -663,8 +663,8 @@ export async function execClaudePrompt(userMessage, options = {}) {
663
663
  const reason = abortReason || controller.signal.reason;
664
664
  const msg =
665
665
  reason === "user_stop"
666
- ? "🛑 Agent stopped by user."
667
- : `⏱️ Agent timed out after ${timeoutMs / 1000}s`;
666
+ ? ":close: Agent stopped by user."
667
+ : `:clock: Agent timed out after ${timeoutMs / 1000}s`;
668
668
  return { finalResponse: msg, items: [], usage: null };
669
669
  }
670
670
  // ── Transient stream retry ──────────────────────────────────────────────────
@@ -696,7 +696,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
696
696
  }
697
697
  const message = err?.message || String(err || "unknown error");
698
698
  return {
699
- finalResponse: `❌ Claude agent failed: ${message}`,
699
+ finalResponse: `:close: Claude agent failed: ${message}`,
700
700
  items: [],
701
701
  usage: null,
702
702
  };
package/cli.mjs CHANGED
@@ -511,7 +511,7 @@ function startDaemon() {
511
511
  // deleted the old codex-monitor directory and its PID file with it).
512
512
  const ghosts = findGhostDaemonPids();
513
513
  if (ghosts.length > 0) {
514
- console.log(` ⚠️ Found ${ghosts.length} ghost bosun daemon process(es) with no PID file: ${ghosts.join(", ")}`);
514
+ console.log(` :alert: Found ${ghosts.length} ghost bosun daemon process(es) with no PID file: ${ghosts.join(", ")}`);
515
515
  console.log(` Stopping ghost process(es) before starting fresh...`);
516
516
  for (const gpid of ghosts) {
517
517
  try { process.kill(gpid, "SIGTERM"); } catch { /* already dead */ }
@@ -526,7 +526,7 @@ function startDaemon() {
526
526
  for (const gpid of alive) {
527
527
  try { process.kill(gpid, "SIGKILL"); } catch { /* ok */ }
528
528
  }
529
- console.log(` Ghost process(es) stopped.`);
529
+ console.log(` :check: Ghost process(es) stopped.`);
530
530
  }
531
531
 
532
532
  // Ensure log directory exists
@@ -673,7 +673,7 @@ function daemonStatus() {
673
673
  // Check for ghost processes (alive but no PID file)
674
674
  const ghosts = findGhostDaemonPids();
675
675
  if (ghosts.length > 0) {
676
- console.log(` ⚠️ bosun daemon is NOT tracked (no PID file), but ${ghosts.length} ghost process(es) found: ${ghosts.join(", ")}`);
676
+ console.log(` :alert: bosun daemon is NOT tracked (no PID file), but ${ghosts.length} ghost process(es) found: ${ghosts.join(", ")}`);
677
677
  console.log(` The daemon is likely running but its PID file was lost.`);
678
678
  console.log(` Run --stop-daemon to clean up, then --daemon to restart.`);
679
679
  } else {
@@ -793,7 +793,7 @@ function terminateBosun() {
793
793
  const killed = allPids.length - alive.length;
794
794
  console.log(` ✓ Terminated ${killed}/${allPids.length} process(es).`);
795
795
  if (alive.length > 0) {
796
- console.log(` ⚠️ Still alive: ${alive.join(", ")}`);
796
+ console.log(` :alert: Still alive: ${alive.join(", ")}`);
797
797
  process.exit(1);
798
798
  return;
799
799
  }
@@ -828,13 +828,13 @@ async function main() {
828
828
  await import("./desktop-shortcut.mjs");
829
829
  const result = installDesktopShortcut();
830
830
  if (result.success) {
831
- console.log(` Desktop shortcut installed (${result.method})`);
831
+ console.log(` :check: Desktop shortcut installed (${result.method})`);
832
832
  if (result.path) console.log(` Path: ${result.path}`);
833
833
  if (result.name) console.log(` Name: ${result.name}`);
834
834
  } else {
835
835
  const method = getDesktopShortcutMethodName();
836
836
  console.error(
837
- ` Failed to install desktop shortcut (${method}): ${result.error}`,
837
+ ` :close: Failed to install desktop shortcut (${method}): ${result.error}`,
838
838
  );
839
839
  }
840
840
  process.exit(result.success ? 0 : 1);
@@ -843,10 +843,10 @@ async function main() {
843
843
  const { removeDesktopShortcut } = await import("./desktop-shortcut.mjs");
844
844
  const result = removeDesktopShortcut();
845
845
  if (result.success) {
846
- console.log(` Desktop shortcut removed`);
846
+ console.log(` :check: Desktop shortcut removed`);
847
847
  } else {
848
848
  console.error(
849
- ` Failed to remove desktop shortcut: ${result.error}`,
849
+ ` :close: Failed to remove desktop shortcut: ${result.error}`,
850
850
  );
851
851
  }
852
852
  process.exit(result.success ? 0 : 1);
@@ -907,7 +907,7 @@ async function main() {
907
907
  const { shouldRunSetup, runSetup } = await import("./setup.mjs");
908
908
  if (shouldRunSetup()) {
909
909
  console.log(
910
- "\n 🚀 First run detected — setup is required before daemon mode.\n",
910
+ "\n :rocket: First run detected — setup is required before daemon mode.\n",
911
911
  );
912
912
  await runSetup();
913
913
  console.log("\n Setup complete. Starting daemon...\n");
@@ -1008,7 +1008,7 @@ async function main() {
1008
1008
  process.env.BOSUN_SENTINEL_STRICT,
1009
1009
  false,
1010
1010
  );
1011
- const prefix = strictSentinel ? "" : "";
1011
+ const prefix = strictSentinel ? ":close:" : ":alert:";
1012
1012
  const suffix = strictSentinel
1013
1013
  ? ""
1014
1014
  : " (continuing without sentinel companion)";
@@ -1235,7 +1235,7 @@ async function main() {
1235
1235
  if (configDirArg) {
1236
1236
  process.env.BOSUN_DIR = configDirArg;
1237
1237
  }
1238
- console.log("\n 🚀 First run detected — launching setup wizard...\n");
1238
+ console.log("\n :rocket: First run detected — launching setup wizard...\n");
1239
1239
  const { startSetupServer } = await import("./setup-web-server.mjs");
1240
1240
  await startSetupServer();
1241
1241
  console.log("\n Setup complete! Starting bosun...\n");
@@ -1246,16 +1246,16 @@ async function main() {
1246
1246
  const legacyInfo = detectLegacySetup();
1247
1247
  if (legacyInfo.hasLegacy && !legacyInfo.alreadyMigrated) {
1248
1248
  console.log(
1249
- `\n 📦 Detected legacy codex-monitor config at ${legacyInfo.legacyDir}`,
1249
+ `\n :box: Detected legacy codex-monitor config at ${legacyInfo.legacyDir}`,
1250
1250
  );
1251
1251
  console.log(` Auto-migrating to ${legacyInfo.newDir}...\n`);
1252
1252
  const result = migrateFromLegacy(legacyInfo.legacyDir, legacyInfo.newDir);
1253
1253
  if (result.migrated.length > 0) {
1254
- console.log(` Migrated: ${result.migrated.join(", ")}`);
1254
+ console.log(` :check: Migrated: ${result.migrated.join(", ")}`);
1255
1255
  console.log(`\n Config is now at ${legacyInfo.newDir}\n`);
1256
1256
  }
1257
1257
  for (const err of result.errors) {
1258
- console.log(` ⚠️ Migration warning: ${err}`);
1258
+ console.log(` :alert: Migration warning: ${err}`);
1259
1259
  }
1260
1260
  }
1261
1261
 
@@ -1428,7 +1428,7 @@ async function sendCrashNotification(exitCode, signal, options = {}) {
1428
1428
  .join("\n")
1429
1429
  : "Monitor is no longer running. Manual restart required.";
1430
1430
  const text =
1431
- `🔥 *CRASH* ${tag} bosun v${VERSION} died unexpectedly\n` +
1431
+ `:zap: *CRASH* ${tag} bosun v${VERSION} died unexpectedly\n` +
1432
1432
  `Host: \`${host}\`\n` +
1433
1433
  `Reason: \`${reason}\`\n` +
1434
1434
  `Time: ${new Date().toISOString()}\n\n` +
@@ -1604,7 +1604,7 @@ function runMonitor({ restartReason = "" } = {}) {
1604
1604
  Math.round(crashState.instantCrashWindowMs / 1000),
1605
1605
  );
1606
1606
  console.error(
1607
- `\n Monitor crashed too quickly ${crashState.instantCrashCount} times in a row (each <= ${windowSec}s, latest ${durationSec}s). Auto-restart is now paused.`,
1607
+ `\n :close: Monitor crashed too quickly ${crashState.instantCrashCount} times in a row (each <= ${windowSec}s, latest ${durationSec}s). Auto-restart is now paused.`,
1608
1608
  );
1609
1609
  sendCrashNotification(exitCode, signal).finally(() =>
1610
1610
  process.exit(exitCode),
@@ -1617,7 +1617,7 @@ function runMonitor({ restartReason = "" } = {}) {
1617
1617
  daemonRestartCount > DAEMON_MAX_RESTARTS
1618
1618
  ) {
1619
1619
  console.error(
1620
- `\n Monitor crashed too many times (${daemonRestartCount - 1} restarts, max ${DAEMON_MAX_RESTARTS}).`,
1620
+ `\n :close: Monitor crashed too many times (${daemonRestartCount - 1} restarts, max ${DAEMON_MAX_RESTARTS}).`,
1621
1621
  );
1622
1622
  sendCrashNotification(exitCode, signal).finally(() =>
1623
1623
  process.exit(exitCode),
@@ -1632,7 +1632,7 @@ function runMonitor({ restartReason = "" } = {}) {
1632
1632
  ? `${daemonRestartCount}/${DAEMON_MAX_RESTARTS}`
1633
1633
  : `${daemonRestartCount}`;
1634
1634
  console.error(
1635
- `\n Monitor exited (${reasonLabel}) — auto-restarting in ${Math.max(1, Math.round(delayMs / 1000))}s${IS_DAEMON_CHILD ? ` [attempt ${attemptLabel}]` : ""}...`,
1635
+ `\n :alert: Monitor exited (${reasonLabel}) — auto-restarting in ${Math.max(1, Math.round(delayMs / 1000))}s${IS_DAEMON_CHILD ? ` [attempt ${attemptLabel}]` : ""}...`,
1636
1636
  );
1637
1637
  sendCrashNotification(exitCode, signal, {
1638
1638
  autoRestartInMs: delayMs,
@@ -1653,7 +1653,7 @@ function runMonitor({ restartReason = "" } = {}) {
1653
1653
 
1654
1654
  if (exitCode !== 0 && !gracefulShutdown) {
1655
1655
  console.error(
1656
- `\n Monitor crashed (${signal ? `signal ${signal}` : `exit code ${exitCode}`}) — sending crash notification...`,
1656
+ `\n :close: Monitor crashed (${signal ? `signal ${signal}` : `exit code ${exitCode}`}) — sending crash notification...`,
1657
1657
  );
1658
1658
  sendCrashNotification(exitCode, signal).finally(() =>
1659
1659
  process.exit(exitCode),
@@ -1668,12 +1668,12 @@ function runMonitor({ restartReason = "" } = {}) {
1668
1668
 
1669
1669
  monitorChild.on("error", (err) => {
1670
1670
  monitorChild = null;
1671
- console.error(`\n Monitor failed to start: ${err.message}`);
1671
+ console.error(`\n :close: Monitor failed to start: ${err.message}`);
1672
1672
  sendCrashNotification(1, null).finally(() => reject(err));
1673
1673
  });
1674
1674
  })
1675
1675
  .catch((err) => {
1676
- console.error(`\n Monitor failed to start: ${err.message}`);
1676
+ console.error(`\n :close: Monitor failed to start: ${err.message}`);
1677
1677
  sendCrashNotification(1, null).finally(() => reject(err));
1678
1678
  });
1679
1679
  });