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/.env.example CHANGED
@@ -105,7 +105,7 @@ TELEGRAM_MINIAPP_ENABLED=false
105
105
  # ║ • Send commands to agents that execute code on YOUR machine ║
106
106
  # ║ • Access secrets, API keys, and environment variables ║
107
107
  # ║ ║
108
- # ║ Combined with TELEGRAM_UI_TUNNEL=auto (Cloudflare tunnel), your UI
108
+ # ║ Combined with TELEGRAM_UI_TUNNEL=named (Cloudflare tunnel), your UI
109
109
  # ║ gets a PUBLIC internet URL — meaning ANYONE ON THE INTERNET can ║
110
110
  # ║ find and control your machine. ║
111
111
  # ║ ║
@@ -120,23 +120,72 @@ TELEGRAM_MINIAPP_ENABLED=false
120
120
  # ── Cloudflare Tunnel (for persistent HTTPS) ────────────────────────────────
121
121
  # Telegram Mini App requires HTTPS with a valid cert. Cloudflare tunnels provide this.
122
122
  #
123
- # Two modes:
124
- # 1. **Quick tunnel** (default): Random *.trycloudflare.com URL, no setup required.
125
- # Pros: Zero config. Cons: URL changes on every restart (refresh Telegram button).
123
+ # Default mode is **named** (permanent hostname, zero tunnel traffic cost):
124
+ # 1. Create tunnel: `cloudflared tunnel create <name>`
125
+ # 2. Save credentials json path
126
+ # 3. Set base domain + Cloudflare DNS API token/zone id
127
+ # Bosun will resolve deterministic per-user hostnames and create/verify the CNAME idempotently.
126
128
  #
127
- # 2. **Named tunnel** (persistent): Custom domain that never changes.
128
- # Setup:
129
- # a) Create tunnel: `cloudflared tunnel create <name>`
130
- # b) Add DNS: `cloudflared tunnel route dns <name> subdomain.yourdomain.com`
131
- # c) Set env vars below.
132
- # Pros: Stable URL (no Telegram button refresh). Cons: Requires Cloudflare account.
133
- #
134
- # Named tunnel env vars (leave blank for quick tunnel):
129
+ # Named tunnel required env:
135
130
  # CLOUDFLARE_TUNNEL_NAME=my-tunnel
136
131
  # CLOUDFLARE_TUNNEL_CREDENTIALS=/home/user/.cloudflared/<tunnel-id>.json
132
+ # CLOUDFLARE_BASE_DOMAIN=bosun.det.io
133
+ # CLOUDFLARE_ZONE_ID=<cloudflare-zone-id>
134
+ # CLOUDFLARE_API_TOKEN=<token-with-zone-dns-edit-scope>
135
+ #
136
+ # Optional overrides:
137
+ # CLOUDFLARE_TUNNEL_HOSTNAME=jon.bosun.det.io
138
+ # CLOUDFLARE_USERNAME_HOSTNAME_POLICY=per-user-fixed # per-user-fixed | fixed
139
+ # CLOUDFLARE_DNS_SYNC_ENABLED=true
140
+ # CLOUDFLARE_DNS_MAX_RETRIES=3
141
+ # CLOUDFLARE_DNS_RETRY_BASE_MS=750
137
142
  #
138
- # Tunnel mode control: auto | cloudflared | disabled
139
- # TELEGRAM_UI_TUNNEL=auto
143
+ # Tunnel mode control: named | quick | auto | cloudflared | disabled
144
+ # TELEGRAM_UI_TUNNEL=named
145
+ # TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK=false
146
+ #
147
+ # Fallback admin auth (secondary path; never stores plaintext credentials):
148
+ # Use API to set/reset credential after startup:
149
+ # POST /api/auth/fallback/set { "secret": "..." }
150
+ # POST /api/auth/fallback/rotate { "secret": "..." }
151
+ # POST /api/auth/fallback/reset
152
+ # POST /api/auth/fallback/login { "secret": "..." }
153
+ # TELEGRAM_UI_FALLBACK_AUTH_ENABLED=true
154
+ # TELEGRAM_UI_FALLBACK_AUTH_RATE_LIMIT_IP_PER_MIN=10
155
+ # TELEGRAM_UI_FALLBACK_AUTH_RATE_LIMIT_GLOBAL_PER_MIN=60
156
+ # TELEGRAM_UI_FALLBACK_AUTH_MAX_FAILURES=5
157
+ # TELEGRAM_UI_FALLBACK_AUTH_LOCKOUT_MS=600000
158
+ # TELEGRAM_UI_FALLBACK_AUTH_ROTATE_DAYS=30
159
+ # TELEGRAM_UI_FALLBACK_AUTH_TRANSIENT_COOLDOWN_MS=5000
160
+
161
+ # ─── Voice Assistant (v0.36+) ───────────────────────────────────────────────
162
+ # Enable real-time voice mode in the UI.
163
+ VOICE_ENABLED=true
164
+ # auto | openai | azure | claude | gemini | fallback
165
+ VOICE_PROVIDER=auto
166
+ # Realtime model (used by openai/azure Tier 1, or as provider-specific default override)
167
+ VOICE_MODEL=gpt-4o-realtime-preview-2024-12-17
168
+ # Vision model for live screen/camera frame understanding
169
+ VOICE_VISION_MODEL=gpt-4.1-mini
170
+ # Optional dedicated key for realtime sessions (falls back to OPENAI_API_KEY)
171
+ # OPENAI_REALTIME_API_KEY=
172
+ # Azure Realtime settings (used when VOICE_PROVIDER=azure, or auto with Azure vars)
173
+ # AZURE_OPENAI_REALTIME_ENDPOINT=https://<resource>.openai.azure.com
174
+ # AZURE_OPENAI_REALTIME_API_KEY=
175
+ # AZURE_OPENAI_REALTIME_DEPLOYMENT=gpt-4o-realtime-preview
176
+ # Claude provider mode (Tier 2 voice fallback + Claude vision)
177
+ # ANTHROPIC_API_KEY=
178
+ # Gemini provider mode (Tier 2 voice fallback + Gemini vision)
179
+ # GEMINI_API_KEY=
180
+ # GOOGLE_API_KEY=
181
+ # Voice output persona
182
+ VOICE_ID=alloy
183
+ # server_vad | semantic_vad | none
184
+ VOICE_TURN_DETECTION=server_vad
185
+ # browser | disabled (used when Tier 1 realtime is unavailable)
186
+ VOICE_FALLBACK_MODE=browser
187
+ # Executor used by voice tool delegations for complex requests
188
+ VOICE_DELEGATE_EXECUTOR=codex-sdk
140
189
 
141
190
  # ─── Desktop Portal ────────────────────────────────────────────────────────
142
191
  # Auto-start bosun daemon when the desktop portal launches (default: true)
@@ -294,7 +343,7 @@ TELEGRAM_MINIAPP_ENABLED=false
294
343
  # INTERNAL_EXECUTOR_BASE_BRANCH_PARALLEL=0
295
344
  # How often to poll kanban for new tasks in ms (default: 30000)
296
345
  # INTERNAL_EXECUTOR_POLL_MS=30000
297
- # SDK to use: "auto" | "codex" | "copilot" | "claude" (default: auto)
346
+ # SDK to use: "auto" | "codex" | "copilot" | "claude" | "gemini" | "opencode" (default: auto)
298
347
  # INTERNAL_EXECUTOR_SDK=auto
299
348
  # Timeout per task execution in ms (default: 5400000 = 90 min)
300
349
  # INTERNAL_EXECUTOR_TIMEOUT_MS=5400000
@@ -316,6 +365,18 @@ TELEGRAM_MINIAPP_ENABLED=false
316
365
  # INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS=2
317
366
  # Require explicit priority for generated tasks (default: true)
318
367
  # INTERNAL_EXECUTOR_REPLENISH_REQUIRE_PRIORITY=true
368
+ # Stream retry ceiling for transient stream disconnects (default: 5)
369
+ # INTERNAL_EXECUTOR_STREAM_MAX_RETRIES=5
370
+ # Stream retry backoff base delay in ms (default: 2000)
371
+ # INTERNAL_EXECUTOR_STREAM_RETRY_BASE_MS=2000
372
+ # Stream retry backoff max delay in ms (default: 32000)
373
+ # INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS=32000
374
+ # Abort/retry turns that emit no stream events within this budget (default: 120000)
375
+ # INTERNAL_EXECUTOR_STREAM_FIRST_EVENT_TIMEOUT_MS=120000
376
+ # Cap number of completed stream items retained per turn (default: 600)
377
+ # INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN=600
378
+ # Truncate oversized item payload strings to this char budget (default: 12000)
379
+ # INTERNAL_EXECUTOR_STREAM_MAX_ITEM_CHARS=12000
319
380
  # Project requirements profile used by planner/replenishment prompts
320
381
  # Allowed: simple-feature | feature | large-feature | system | multi-system
321
382
  # PROJECT_REQUIREMENTS_PROFILE=feature
@@ -653,7 +714,7 @@ VK_RECOVERY_PORT=54089
653
714
  # Set to true to disable all Codex/AI features (analysis, autofix, shell)
654
715
  # CODEX_SDK_DISABLED=false
655
716
 
656
- # Primary agent adapter: codex-sdk | copilot-sdk | claude-sdk
717
+ # Primary agent adapter: codex-sdk | copilot-sdk | claude-sdk | gemini-sdk | opencode-sdk
657
718
  # PRIMARY_AGENT=codex-sdk
658
719
  # Set to true to disable the primary agent adapter
659
720
  # PRIMARY_AGENT_DISABLED=false
@@ -834,6 +895,27 @@ VK_RECOVERY_PORT=54089
834
895
  # ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
835
896
  # CLAUDE_API_KEY=your-anthropic-api-key
836
897
  # CLAUDE_KEY=your-anthropic-api-key
898
+
899
+ # ─── Gemini SDK ──────────────────────────────────────────────────────────────
900
+ # Set to true to disable Gemini SDK features
901
+ # GEMINI_SDK_DISABLED=false
902
+ # Transport selector: auto | sdk | cli
903
+ # GEMINI_TRANSPORT=auto
904
+ # Gemini model (default: gemini-2.5-pro)
905
+ # GEMINI_MODEL=gemini-2.5-pro
906
+ # API key (either variable works)
907
+ # GEMINI_API_KEY=
908
+ # GOOGLE_API_KEY=
909
+ # Optional Gemini API base URL override
910
+ # GEMINI_BASE_URL=
911
+
912
+ # ─── OpenCode SDK ────────────────────────────────────────────────────────────
913
+ # Set to true to disable OpenCode SDK features
914
+ # OPENCODE_SDK_DISABLED=false
915
+ # Local OpenCode server port
916
+ # OPENCODE_PORT=4096
917
+ # Optional model override passed to OpenCode
918
+ # OPENCODE_MODEL=gpt-5.2-codex
837
919
  # ─── Merge Strategy (Codex-powered PR decision engine) ───────────────────────
838
920
  # When a task completes, analyze the agent's output via Codex SDK to decide:
839
921
  # merge_after_ci_pass, prompt (agent), close_pr, re_attempt, manual_review, wait
package/README.md CHANGED
@@ -44,6 +44,33 @@ Requires:
44
44
 
45
45
  ---
46
46
 
47
+ ## Permanent Mini App Hostname + Fallback Auth
48
+
49
+ Bosun defaults the Mini App tunnel to **named** mode so the Telegram URL can stay stable (`<user>.<base-domain>`), with quick tunnels only as explicit fallback.
50
+
51
+ Required Cloudflare settings:
52
+
53
+ - `CLOUDFLARE_TUNNEL_NAME`
54
+ - `CLOUDFLARE_TUNNEL_CREDENTIALS`
55
+ - `CLOUDFLARE_BASE_DOMAIN` (for example `bosun.det.io`)
56
+ - `CLOUDFLARE_ZONE_ID`
57
+ - `CLOUDFLARE_API_TOKEN` (Zone DNS edit scope for the target zone)
58
+
59
+ Useful optional settings:
60
+
61
+ - `CLOUDFLARE_TUNNEL_HOSTNAME` (explicit hostname override)
62
+ - `CLOUDFLARE_USERNAME_HOSTNAME_POLICY=per-user-fixed`
63
+ - `TELEGRAM_UI_ALLOW_QUICK_TUNNEL_FALLBACK=false`
64
+
65
+ Fallback admin auth (secondary path) is available and stores only Argon2id hash + salt, never plaintext. Use:
66
+
67
+ - `POST /api/auth/fallback/set` to set/rotate
68
+ - `POST /api/auth/fallback/rotate` as explicit rotate alias
69
+ - `POST /api/auth/fallback/reset` to clear
70
+ - `POST /api/auth/fallback/login` to mint normal `ve_session` cookie
71
+
72
+ ---
73
+
47
74
  ## What Bosun does
48
75
 
49
76
  - Routes work across Codex, Copilot, Claude, and OpenCode executors
@@ -387,7 +387,7 @@ export class AgentEventBus {
387
387
  if (this._sendTelegram) {
388
388
  const task = this._resolveTask(taskId);
389
389
  const title = task?.title || taskId;
390
- this._sendTelegram(`🛑 Task blocked: "${title}" (source: ${source})`);
390
+ this._sendTelegram(`:close: Task blocked: "${title}" (source: ${source})`);
391
391
  }
392
392
  }
393
393
  }
@@ -401,7 +401,7 @@ export class AgentEventBus {
401
401
  reason: reason || "manual",
402
402
  });
403
403
  if (this._sendTelegram) {
404
- this._sendTelegram(`⏸️ Executor paused: ${reason || "manual"}`);
404
+ this._sendTelegram(`:pause: Executor paused: ${reason || "manual"}`);
405
405
  }
406
406
  }
407
407
 
@@ -662,7 +662,7 @@ export class AgentEventBus {
662
662
  const task = this._resolveTask(taskId);
663
663
  const title = task?.title || taskId;
664
664
  this._sendTelegram(
665
- `🛑 Auto-blocked: "${title}" — ${recovery?.reason || "too many errors"}`,
665
+ `:close: Auto-blocked: "${title}" — ${recovery?.reason || "too many errors"}`,
666
666
  );
667
667
  }
668
668
  console.log(
@@ -694,7 +694,7 @@ export class AgentEventBus {
694
694
  });
695
695
  if (this._sendTelegram) {
696
696
  this._sendTelegram(
697
- `⏸️ Executor auto-paused: ${recovery?.reason || "rate limit flood"}`,
697
+ `:pause: Executor auto-paused: ${recovery?.reason || "rate limit flood"}`,
698
698
  );
699
699
  }
700
700
  console.log(`${TAG} executor paused: ${recovery?.reason}`);
@@ -710,7 +710,7 @@ export class AgentEventBus {
710
710
  const task = this._resolveTask(taskId);
711
711
  const title = task?.title || taskId;
712
712
  this._sendTelegram(
713
- `⚠️ "${title}" needs manual review: ${recovery?.reason || "repeated errors"}`,
713
+ `:alert: "${title}" needs manual review: ${recovery?.reason || "repeated errors"}`,
714
714
  );
715
715
  }
716
716
  break;
package/agent-pool.mjs CHANGED
@@ -95,6 +95,117 @@ const MAX_PROMPT_BYTES = 180_000;
95
95
  const MAX_SET_TIMEOUT_MS = 2_147_483_647; // Node.js setTimeout 32-bit signed max
96
96
  let timeoutClampWarningKey = "";
97
97
  const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 120_000;
98
+ const DEFAULT_MAX_ITEMS_PER_TURN = 600;
99
+ const DEFAULT_MAX_ITEM_CHARS = 12_000;
100
+ const TOOL_OUTPUT_GUARDRAIL = String.raw`
101
+
102
+ [Tool Output Guardrail] Keep tool outputs compact: prefer narrow searches, bounded command output (for example head/tail), and summaries for large results instead of dumping full payloads.`;
103
+
104
+ function parseBoundedNumber(value, fallback, min, max) {
105
+ const num = Number(value);
106
+ if (!Number.isFinite(num)) return fallback;
107
+ return Math.min(Math.max(Math.trunc(num), min), max);
108
+ }
109
+
110
+ function getInternalExecutorStreamConfig() {
111
+ try {
112
+ const cfg = loadConfig();
113
+ const stream = cfg?.internalExecutor?.stream;
114
+ return stream && typeof stream === "object" ? stream : {};
115
+ } catch {
116
+ return {};
117
+ }
118
+ }
119
+
120
+ function truncateText(text, maxChars) {
121
+ if (typeof text !== "string") return text;
122
+ if (!Number.isFinite(maxChars) || maxChars < 1 || text.length <= maxChars) {
123
+ return text;
124
+ }
125
+ const trimmed = text.slice(0, maxChars);
126
+ const removed = text.length - maxChars;
127
+ return `${trimmed}
128
+
129
+ […truncated ${removed} chars…]`;
130
+ }
131
+
132
+ function truncateItemForStorage(item, maxChars) {
133
+ if (!item || typeof item !== "object") return item;
134
+ if (!Number.isFinite(maxChars) || maxChars < 1) return item;
135
+
136
+ const next = { ...item };
137
+ const directStringKeys = [
138
+ "text",
139
+ "output",
140
+ "aggregated_output",
141
+ "stderr",
142
+ "stdout",
143
+ "result",
144
+ "message",
145
+ ];
146
+ for (const key of directStringKeys) {
147
+ if (typeof next[key] === "string") {
148
+ next[key] = truncateText(next[key], maxChars);
149
+ }
150
+ }
151
+
152
+ if (Array.isArray(next.content)) {
153
+ next.content = next.content.map((entry) => {
154
+ if (entry && typeof entry === "object" && typeof entry.text === "string") {
155
+ return { ...entry, text: truncateText(entry.text, maxChars) };
156
+ }
157
+ return entry;
158
+ });
159
+ }
160
+
161
+ if (next.error && typeof next.error === "object") {
162
+ next.error = {
163
+ ...next.error,
164
+ message: truncateText(next.error.message, maxChars),
165
+ };
166
+ }
167
+
168
+ return next;
169
+ }
170
+
171
+ function resolveCodexStreamSafety(totalTimeoutMs) {
172
+ const streamCfg = getInternalExecutorStreamConfig();
173
+ const firstEventRaw =
174
+ process.env.INTERNAL_EXECUTOR_STREAM_FIRST_EVENT_TIMEOUT_MS ||
175
+ process.env.AGENT_POOL_FIRST_EVENT_TIMEOUT_MS ||
176
+ streamCfg.firstEventTimeoutMs ||
177
+ DEFAULT_FIRST_EVENT_TIMEOUT_MS;
178
+ const maxItemsRaw =
179
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN ||
180
+ streamCfg.maxItemsPerTurn ||
181
+ DEFAULT_MAX_ITEMS_PER_TURN;
182
+ const maxItemCharsRaw =
183
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEM_CHARS ||
184
+ streamCfg.maxItemChars ||
185
+ DEFAULT_MAX_ITEM_CHARS;
186
+
187
+ const configuredFirstEventMs = parseBoundedNumber(
188
+ firstEventRaw,
189
+ DEFAULT_FIRST_EVENT_TIMEOUT_MS,
190
+ 1_000,
191
+ 60 * 60 * 1000,
192
+ );
193
+ const budgetMs = Number(totalTimeoutMs);
194
+ let firstEventTimeoutMs = null;
195
+ if (Number.isFinite(budgetMs) && budgetMs > 2_000) {
196
+ const maxAllowed = Math.max(1_000, budgetMs - 1_000);
197
+ firstEventTimeoutMs = clampTimerDelayMs(
198
+ Math.min(configuredFirstEventMs, maxAllowed),
199
+ "first-event-timeout",
200
+ );
201
+ }
202
+
203
+ return {
204
+ firstEventTimeoutMs,
205
+ maxItemsPerTurn: parseBoundedNumber(maxItemsRaw, DEFAULT_MAX_ITEMS_PER_TURN, 1, 5000),
206
+ maxItemChars: parseBoundedNumber(maxItemCharsRaw, DEFAULT_MAX_ITEM_CHARS, 1, 250000),
207
+ };
208
+ }
98
209
 
99
210
  function clampTimerDelayMs(delayMs, label = "timer") {
100
211
  const parsed = Number(delayMs);
@@ -113,14 +224,7 @@ function clampTimerDelayMs(delayMs, label = "timer") {
113
224
  }
114
225
 
115
226
  function getFirstEventTimeoutMs(totalTimeoutMs) {
116
- const configured = Number(
117
- process.env.AGENT_POOL_FIRST_EVENT_TIMEOUT_MS || DEFAULT_FIRST_EVENT_TIMEOUT_MS,
118
- );
119
- if (!Number.isFinite(configured) || configured <= 0) return null;
120
- const budgetMs = Number(totalTimeoutMs);
121
- if (!Number.isFinite(budgetMs) || budgetMs <= 2_000) return null;
122
- const maxAllowed = Math.max(5_000, budgetMs - 1_000);
123
- return clampTimerDelayMs(Math.min(Math.trunc(configured), maxAllowed), "first-event-timeout");
227
+ return resolveCodexStreamSafety(totalTimeoutMs).firstEventTimeoutMs;
124
228
  }
125
229
 
126
230
  function sanitizeAndBoundPrompt(text) {
@@ -982,13 +1086,15 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
982
1086
 
983
1087
  // ── 4. Stream the turn ───────────────────────────────────────────────────
984
1088
  try {
985
- const safePrompt = sanitizeAndBoundPrompt(prompt);
1089
+ const streamSafety = resolveCodexStreamSafety(timeoutMs);
1090
+ const safePrompt = sanitizeAndBoundPrompt(`${prompt}${TOOL_OUTPUT_GUARDRAIL}`);
986
1091
  const turn = await thread.runStreamed(safePrompt, {
987
1092
  signal: controller.signal,
988
1093
  });
989
1094
 
990
1095
  let finalResponse = "";
991
1096
  const allItems = [];
1097
+ let droppedItems = 0;
992
1098
  // Race the event iterator against a hard timeout.
993
1099
  // The soft timeout fires controller.abort() which the SDK should honor.
994
1100
  // The hard timeout is a safety net in case the SDK iterator ignores the abort.
@@ -1018,7 +1124,11 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
1018
1124
  }
1019
1125
  }
1020
1126
  if (event.type === "item.completed") {
1021
- allItems.push(event.item);
1127
+ if (allItems.length < streamSafety.maxItemsPerTurn) {
1128
+ allItems.push(truncateItemForStorage(event.item, streamSafety.maxItemChars));
1129
+ } else {
1130
+ droppedItems += 1;
1131
+ }
1022
1132
  if (event.item.type === "agent_message" && event.item.text) {
1023
1133
  finalResponse += event.item.text + "\n";
1024
1134
  }
@@ -1026,7 +1136,7 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
1026
1136
  }
1027
1137
  };
1028
1138
 
1029
- const firstEventTimeoutMs = getFirstEventTimeoutMs(timeoutMs);
1139
+ const firstEventTimeoutMs = streamSafety.firstEventTimeoutMs;
1030
1140
  if (firstEventTimeoutMs) {
1031
1141
  firstEventTimer = setTimeout(() => {
1032
1142
  if (eventCount > 0 || controller.signal.aborted) return;
@@ -1041,6 +1151,13 @@ async function launchCodexThread(prompt, cwd, timeoutMs, extra = {}) {
1041
1151
  if (firstEventTimer) clearTimeout(firstEventTimer);
1042
1152
  clearAbortScope();
1043
1153
 
1154
+ if (droppedItems > 0) {
1155
+ allItems.push({
1156
+ type: "stream_notice",
1157
+ text: `Dropped ${droppedItems} completed items to stay within INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN=${streamSafety.maxItemsPerTurn}.`,
1158
+ });
1159
+ }
1160
+
1044
1161
  const output =
1045
1162
  finalResponse.trim() || "(Agent completed with no text output)";
1046
1163
  if (steerKey) unregisterActiveSession(steerKey);
@@ -2786,7 +2903,7 @@ export async function launchOrResumeThread(
2786
2903
  ) {
2787
2904
  const remaining = MAX_THREAD_TURNS - existing.turnCount;
2788
2905
  console.warn(
2789
- `${TAG} thread for task "${taskKey}" approaching exhaustion: ${existing.turnCount}/${MAX_THREAD_TURNS} turns (${remaining} remaining)`,
2906
+ `${TAG} :alert: thread for task "${taskKey}" approaching exhaustion: ${existing.turnCount}/${MAX_THREAD_TURNS} turns (${remaining} remaining)`,
2790
2907
  );
2791
2908
  }
2792
2909
 
package/agent-prompts.mjs CHANGED
@@ -208,13 +208,17 @@ You generate production-grade backlog tasks for autonomous executors.
208
208
  - Do not call any kanban API, CLI, or external service to create tasks.
209
209
  The workflow will automatically materialize your output into kanban tasks.
210
210
  - Output must be machine-parseable JSON — see Output Contract below.
211
+ - Task objects must be valid for Bosun backlog creation with fields:
212
+ \'title\', \'description\', \'implementation_steps\', \'acceptance_criteria\',
213
+ \'verification\', optional \'base_branch\'.
214
+ - Do not emit empty or placeholder tasks. Every task must be actionable and execution-ready.
211
215
 
212
216
  ## Output Contract (MANDATORY — STRICT)
213
217
 
214
218
  Your ENTIRE response must be a single fenced JSON block. Do NOT include any
215
219
  text, commentary, explanations, or markdown before or after the JSON block.
216
220
  The downstream parser extracts JSON from fenced blocks — any deviation causes
217
- task creation to fail silently.
221
+ task creation to hard-fail.
218
222
 
219
223
  Return exactly this shape:
220
224
 
@@ -238,6 +242,8 @@ Rules:
238
242
  - Do NOT output partial JSON, truncated arrays, or commentary mixed with JSON.
239
243
  - Keep titles unique and specific.
240
244
  - Keep file overlap low across tasks to maximize parallel execution.
245
+ - Descriptions must include concrete implementation details, not generic intent text.
246
+ - Include verification commands/checks that a worker can run without additional planning.
241
247
  - **Module branch routing:** When the task title follows conventional commit format
242
248
  \`feat(module):\` or \`fix(module):\`, set \`base_branch\` to \`origin/<module>\`.
243
249
  This routes the task to the module's dedicated branch for parallel, isolated development.
package/agent-sdk.mjs CHANGED
@@ -4,13 +4,19 @@
4
4
  * Reads ~/.codex/config.toml to determine the primary agent SDK and
5
5
  * capability flags for bosun integrations.
6
6
  *
7
- * Supported primary agents: "codex", "copilot", "claude"
7
+ * Supported primary agents: "codex", "copilot", "claude", "opencode", "gemini"
8
8
  * Capability flags: steering, subagents, vscode_tools
9
9
  */
10
10
 
11
11
  import { readCodexConfig } from "./codex-config.mjs";
12
12
 
13
- const SUPPORTED_PRIMARY = new Set(["codex", "copilot", "claude", "opencode"]);
13
+ const SUPPORTED_PRIMARY = new Set([
14
+ "codex",
15
+ "copilot",
16
+ "claude",
17
+ "opencode",
18
+ "gemini",
19
+ ]);
14
20
  const DEFAULT_PRIMARY = "codex";
15
21
 
16
22
  const DEFAULT_CAPABILITIES_BY_PRIMARY = {
@@ -34,6 +40,11 @@ const DEFAULT_CAPABILITIES_BY_PRIMARY = {
34
40
  subagents: true,
35
41
  vscodeTools: false,
36
42
  },
43
+ gemini: {
44
+ steering: false,
45
+ subagents: true,
46
+ vscodeTools: false,
47
+ },
37
48
  };
38
49
 
39
50
  const DEFAULT_CAPABILITIES = {
@@ -516,7 +516,7 @@ export class AgentSupervisor {
516
516
  const state = this._ensureTaskState(taskId);
517
517
  if (this._sendTelegram) {
518
518
  this._sendTelegram(
519
- `🛑 Supervisor blocked "${title}": ${reason}\n` +
519
+ `:close: Supervisor blocked "${title}": ${reason}\n` +
520
520
  `Situation: ${situation}, Interventions attempted: ${state.interventionCount}`,
521
521
  );
522
522
  }
@@ -535,7 +535,7 @@ export class AgentSupervisor {
535
535
  this._pauseExecutor(5 * 60_000, reason);
536
536
  }
537
537
  if (this._sendTelegram) {
538
- this._sendTelegram(`⏸️ Executor paused by supervisor: ${reason}`);
538
+ this._sendTelegram(`:pause: Executor paused by supervisor: ${reason}`);
539
539
  }
540
540
  break;
541
541
 
@@ -200,7 +200,7 @@ export function formatWeeklyAgentWorkReport(summary) {
200
200
  : buildWeeklyAgentWorkSummary({ metrics: [], errors: [] });
201
201
  const totals = safeSummary.totals || {};
202
202
  const lines = [
203
- "📊 Weekly Agent Work Report",
203
+ ":chart: Weekly Agent Work Report",
204
204
  `Period: ${safeSummary.period?.startIso || "n/a"} → ${safeSummary.period?.endIso || "n/a"}`,
205
205
  `Generated: ${safeSummary.period?.generatedAtIso || new Date().toISOString()}`,
206
206
  "",
@@ -526,7 +526,7 @@ export class AnomalyDetector {
526
526
  const s = this.getStats();
527
527
  const uptimeMin = Math.round(s.uptimeMs / 60_000);
528
528
  const lines = [
529
- `<b>🔍 Anomaly Detector Status</b>`,
529
+ `<b>:search: Anomaly Detector Status</b>`,
530
530
  `Uptime: ${uptimeMin}m | Lines: ${s.totalLinesProcessed.toLocaleString()}`,
531
531
  `Active: ${s.activeProcesses} | Completed: ${s.completedProcesses}`,
532
532
  ];
@@ -573,7 +573,7 @@ export class AnomalyDetector {
573
573
  }
574
574
  if (concerns.length > 0) {
575
575
  lines.push(
576
- `\n⚠️ <b>${escapeHtml(proc.shortId)}</b> (${escapeHtml(proc.taskTitle || "?")}):`,
576
+ `\n:alert: <b>${escapeHtml(proc.shortId)}</b> (${escapeHtml(proc.taskTitle || "?")}):`,
577
577
  ` ${concerns.join(", ")}`,
578
578
  );
579
579
  }
@@ -1202,13 +1202,13 @@ export class AnomalyDetector {
1202
1202
  anomaly.severity === Severity.CRITICAL ||
1203
1203
  anomaly.severity === Severity.HIGH
1204
1204
  ) {
1205
- const icon = anomaly.severity === Severity.CRITICAL ? "🔴" : "🟠";
1205
+ const icon = anomaly.severity === Severity.CRITICAL ? ":dot:" : ":u1f7e0:";
1206
1206
  const actionLabel =
1207
1207
  anomaly.action === "kill"
1208
- ? " KILL"
1208
+ ? ":ban: KILL"
1209
1209
  : anomaly.action === "restart"
1210
- ? "🔄 RESTART"
1211
- : "⚠️ ALERT";
1210
+ ? ":refresh: RESTART"
1211
+ : ":alert: ALERT";
1212
1212
 
1213
1213
  const msg = [
1214
1214
  `${icon} <b>Anomaly: ${escapeHtml(anomaly.type)}</b>`,