bosun 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +301 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +742 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +224 -22
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +12 -12
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +132 -15
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +89 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +57 -32
- package/ui/styles/components.css +47 -0
- package/ui/tabs/agents.js +31 -31
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1760 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- 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=
|
|
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
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
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
|
-
#
|
|
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=
|
|
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
|
package/agent-event-bus.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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}
|
|
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
|
|
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([
|
|
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 = {
|
package/agent-supervisor.mjs
CHANGED
|
@@ -516,7 +516,7 @@ export class AgentSupervisor {
|
|
|
516
516
|
const state = this._ensureTaskState(taskId);
|
|
517
517
|
if (this._sendTelegram) {
|
|
518
518
|
this._sendTelegram(
|
|
519
|
-
|
|
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(
|
|
538
|
+
this._sendTelegram(`:pause: Executor paused by supervisor: ${reason}`);
|
|
539
539
|
}
|
|
540
540
|
break;
|
|
541
541
|
|
package/agent-work-report.mjs
CHANGED
|
@@ -200,7 +200,7 @@ export function formatWeeklyAgentWorkReport(summary) {
|
|
|
200
200
|
: buildWeeklyAgentWorkSummary({ metrics: [], errors: [] });
|
|
201
201
|
const totals = safeSummary.totals || {};
|
|
202
202
|
const lines = [
|
|
203
|
-
"
|
|
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
|
"",
|
package/anomaly-detector.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
? "
|
|
1208
|
+
? ":ban: KILL"
|
|
1209
1209
|
: anomaly.action === "restart"
|
|
1210
|
-
? "
|
|
1211
|
-
: "
|
|
1210
|
+
? ":refresh: RESTART"
|
|
1211
|
+
: ":alert: ALERT";
|
|
1212
1212
|
|
|
1213
1213
|
const msg = [
|
|
1214
1214
|
`${icon} <b>Anomaly: ${escapeHtml(anomaly.type)}</b>`,
|