bosun 0.35.4 → 0.36.0

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 (42) hide show
  1. package/agent-prompts.mjs +6 -7
  2. package/agent-supervisor.mjs +3 -3
  3. package/autofix.mjs +1 -1
  4. package/bosun-skills.mjs +6 -14
  5. package/bosun.schema.json +67 -0
  6. package/claude-shell.mjs +19 -2
  7. package/codex-shell.mjs +50 -16
  8. package/config.mjs +3 -0
  9. package/copilot-shell.mjs +16 -3
  10. package/opencode-shell.mjs +14 -1
  11. package/package.json +1 -1
  12. package/session-tracker.mjs +135 -0
  13. package/setup.mjs +4 -4
  14. package/task-executor.mjs +50 -119
  15. package/telegram-bot.mjs +90 -0
  16. package/ui/components/chat-view.js +50 -22
  17. package/ui/components/session-list.js +9 -1
  18. package/ui/demo.html +22 -22
  19. package/ui/modules/audio-visualizer.js +156 -0
  20. package/ui/modules/settings-schema.js +14 -0
  21. package/ui/modules/voice-client.js +451 -0
  22. package/ui/modules/voice-fallback.js +208 -0
  23. package/ui/modules/voice-overlay.js +341 -0
  24. package/ui/styles/components.css +43 -386
  25. package/ui/styles/kanban.css +91 -17
  26. package/ui/styles/layout.css +37 -8
  27. package/ui/tabs/agents.js +1 -1
  28. package/ui/tabs/chat.js +31 -2
  29. package/ui/tabs/tasks.js +86 -266
  30. package/ui/tabs/workflows.js +36 -5
  31. package/ui-server.mjs +99 -5
  32. package/ve-kanban.ps1 +11 -11
  33. package/ve-orchestrator.ps1 +30 -63
  34. package/workflow-engine.mjs +31 -7
  35. package/workflow-nodes.mjs +25 -26
  36. package/workflow-templates/agents.mjs +18 -17
  37. package/workflow-templates/ci-cd.mjs +9 -3
  38. package/workflow-templates/github.mjs +9 -5
  39. package/workflow-templates/planning.mjs +21 -12
  40. package/workflow-templates/reliability.mjs +26 -14
  41. package/workflow-templates/security.mjs +53 -46
  42. package/workspace-manager.mjs +88 -13
package/agent-prompts.mjs CHANGED
@@ -437,7 +437,7 @@ You are the always-on reliability guardian for bosun in devmode.
437
437
  ## Constraints
438
438
 
439
439
  - Operate only in devmode.
440
- - Do not commit/push/open PRs in this context.
440
+ - Do not commit/push/initiate PR lifecycle changes in this context.
441
441
  - Apply focused fixes, run focused validation, and keep monitoring.
442
442
  `,
443
443
  taskExecutor: `# {{TASK_ID}} — {{TASK_TITLE}}
@@ -495,7 +495,7 @@ These patterns have caused real production crashes. Treat them as hard rules:
495
495
  add config overrides that bypass safety checks. If a feature is behind a flag,
496
496
  respect it.
497
497
 
498
- ## Bosun Task Agent — Git & PR Workflow
498
+ ## Bosun Task Agent — Git & Bosun Lifecycle Workflow
499
499
 
500
500
  You are running as a **Bosun-managed task agent**. Environment variables
501
501
  \`BOSUN_TASK_TITLE\`, \`BOSUN_BRANCH_NAME\`, \`BOSUN_TASK_ID\`, and their
@@ -512,18 +512,17 @@ You are running as a **Bosun-managed task agent**. Environment variables
512
512
  \`git fetch origin && git merge origin/<base-branch> --no-edit && git merge origin/main --no-edit\`
513
513
  Resolve any conflicts that arise before pushing.
514
514
  - Push: \`git push --set-upstream origin {{BRANCH}}\`
515
- - After a successful push, open a Pull Request:
516
- \`gh pr create --title "{{TASK_TITLE}}" --body "Closes task {{TASK_ID}}"\`
517
- - **Do NOT** run \`gh pr merge\` — the orchestrator handles merges after CI.
515
+ - After a successful push, hand off PR lifecycle to Bosun management.
516
+ - Do not run direct PR commands.
518
517
  {{COAUTHOR_INSTRUCTION}}
519
518
  **Do NOT:**
520
519
  - Bypass pre-push hooks (\`git push --no-verify\` is forbidden).
521
520
  - Use \`git add .\` — stage files individually.
522
- - Wait for user confirmation before pushing or opening the PR.
521
+ - Wait for user confirmation before pushing or handing off lifecycle state.
523
522
 
524
523
  ## Agent Status Endpoint
525
524
  - URL: http://127.0.0.1:{{ENDPOINT_PORT}}/api/tasks/{{TASK_ID}}
526
- - POST /status {"status":"inreview"} after PR-ready push
525
+ - POST /status {"status":"inreview"} after push + Bosun lifecycle handoff readiness
527
526
  - POST /heartbeat {} while running
528
527
  - POST /error {"error":"..."} on fatal failure
529
528
  - POST /complete {"hasCommits":true} when done
@@ -192,9 +192,9 @@ const RECOVERY_PROMPTS = {
192
192
  `If push fails due to pre-push hooks, fix the issues and push again.`,
193
193
 
194
194
  [SITUATION.PR_NOT_CREATED]: (ctx) =>
195
- `You pushed commits for "${ctx.taskTitle}" but no PR was created. Run:\n` +
196
- `gh pr create --title "${ctx.taskTitle}" --body "Automated PR" --head ${ctx.branch || "$(git branch --show-current)"}\n` +
197
- `If gh is not available, the system will create the PR automatically.`,
195
+ `You pushed commits for "${ctx.taskTitle}" but no PR is visible yet.\n` +
196
+ `Direct PR commands are disabled. Confirm the branch is pushed, then mark this run as ready for Bosun-managed PR lifecycle handoff.\n` +
197
+ `Do not run direct PR-create commands.`,
198
198
 
199
199
  [SITUATION.TOOL_LOOP]: (ctx) =>
200
200
  `You've been repeating the same tools (${ctx.loopedTools || "unknown"}) without progress. ` +
package/autofix.mjs CHANGED
@@ -1270,7 +1270,7 @@ ${messagesCtx}
1270
1270
  3. Identify why it loops (missing break/continue/return, no state change between iterations, etc.)
1271
1271
  4. Fix the loop by adding proper exit conditions, error handling, or state tracking
1272
1272
  5. Common loop-causing patterns in this codebase:
1273
- - \`gh pr create\` failing with "No commits between" but caller retries every cycle
1273
+ - PR lifecycle handoff repeatedly retried with no diff between branch and base
1274
1274
  - API calls returning the same error repeatedly with no backoff or give-up logic
1275
1275
  - Status not updated after failure → next cycle tries the same thing
1276
1276
  - Missing \`continue\` or state change in foreach loops over tracked attempts
package/bosun-skills.mjs CHANGED
@@ -114,7 +114,7 @@ Your worktree path is provided via \`BOSUN_WORKTREE_PATH\`. Stay inside it.
114
114
  scope: "global",
115
115
  content: `# Skill: Pull Request Workflow
116
116
 
117
- ## Standard PR Flow
117
+ ## Standard Bosun Lifecycle Flow
118
118
 
119
119
  After committing all changes on your task branch:
120
120
 
@@ -127,11 +127,11 @@ git merge origin/main --no-edit 2>/dev/null || true
127
127
  # Resolve any conflicts, commit, then push
128
128
  git push --set-upstream origin <branch-name>
129
129
 
130
- # Open the PR
131
- gh pr create --title "<task-title>" --body "Closes task <task-id>\\n\\n## Summary\\n<one-paragraph summary>"
130
+ # Hand off PR lifecycle to Bosun manager (no direct PR-create command)
131
+ echo "PR lifecycle handoff ready for <branch-name>"
132
132
  \`\`\`
133
133
 
134
- **Do NOT** run \`gh pr merge\` the orchestrator handles CI monitoring and merging.
134
+ Bosun manages PR lifecycle (create/update/merge) after handoff.
135
135
 
136
136
  ## PR Description Template
137
137
 
@@ -158,17 +158,9 @@ Bosun installs pre-push hooks that run build + test validation.
158
158
  If the hook runs \`npm test\` or \`dotnet test\` and fails:
159
159
  1. Read the test output carefully.
160
160
  2. Fix the root cause (not just suppress the error).
161
- 3. If the failure is in an unrelated existing test, note it in the PR description
161
+ 3. If the failure is in an unrelated existing test, note it in the lifecycle handoff context
162
162
  and run a targeted test to confirm your changes don't regress it.
163
163
 
164
- ## Draft PRs
165
-
166
- Use \`--draft\` when the implementation is complete but CI is long-running:
167
- \`\`\`bash
168
- gh pr create --draft --title "..." --body "..."
169
- \`\`\`
170
- Convert to ready when CI passes: \`gh pr ready <number>\`
171
-
172
164
  ## Reviewing CI Status
173
165
 
174
166
  \`\`\`bash
@@ -427,7 +419,7 @@ Your branch was created from that base — not from \`main\` directly.
427
419
  Merge order on completion:
428
420
  1. Merge upstream base branch changes into your branch (keeps drift low).
429
421
  2. Merge main (catches global changes like dep bumps).
430
- 3. Push and open PR targeting the base branch.
422
+ 3. Push and hand off lifecycle targeting the base branch.
431
423
 
432
424
  The orchestrator then merges the base branch into main after CI.
433
425
 
package/bosun.schema.json CHANGED
@@ -40,6 +40,73 @@
40
40
  "type": "string",
41
41
  "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"]
42
42
  },
43
+ "voice": {
44
+ "type": "object",
45
+ "description": "Voice assistant configuration for real-time voice interaction",
46
+ "additionalProperties": false,
47
+ "properties": {
48
+ "enabled": {
49
+ "type": "boolean",
50
+ "default": true,
51
+ "description": "Enable voice assistant features"
52
+ },
53
+ "provider": {
54
+ "type": "string",
55
+ "enum": ["openai", "azure", "fallback", "auto"],
56
+ "default": "auto",
57
+ "description": "Voice provider: openai (direct API), azure (Azure OpenAI), fallback (browser STT/TTS), auto (detect from env)"
58
+ },
59
+ "model": {
60
+ "type": "string",
61
+ "default": "gpt-4o-realtime-preview-2024-12-17",
62
+ "description": "Realtime API model name"
63
+ },
64
+ "openaiApiKey": {
65
+ "type": "string",
66
+ "description": "OpenAI API key for Realtime API (overrides OPENAI_API_KEY env)"
67
+ },
68
+ "azureApiKey": {
69
+ "type": "string",
70
+ "description": "Azure OpenAI API key for Realtime API"
71
+ },
72
+ "azureEndpoint": {
73
+ "type": "string",
74
+ "description": "Azure OpenAI endpoint URL"
75
+ },
76
+ "azureDeployment": {
77
+ "type": "string",
78
+ "default": "gpt-4o-realtime-preview",
79
+ "description": "Azure OpenAI deployment name"
80
+ },
81
+ "voiceId": {
82
+ "type": "string",
83
+ "enum": ["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"],
84
+ "default": "alloy",
85
+ "description": "Voice ID for TTS output"
86
+ },
87
+ "turnDetection": {
88
+ "type": "string",
89
+ "enum": ["server_vad", "semantic_vad", "none"],
90
+ "default": "server_vad",
91
+ "description": "Turn detection mode for voice activity detection"
92
+ },
93
+ "instructions": {
94
+ "type": "string",
95
+ "description": "Custom system instructions for the voice assistant"
96
+ },
97
+ "fallbackMode": {
98
+ "type": "string",
99
+ "enum": ["browser", "disabled"],
100
+ "default": "browser",
101
+ "description": "Fallback when Realtime API unavailable: browser (Web Speech API) or disabled"
102
+ },
103
+ "delegateExecutor": {
104
+ "type": "string",
105
+ "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "opencode-sdk"],
106
+ "description": "Which executor to use for delegate_to_agent calls. Defaults to primaryAgent."
107
+ }
108
+ }
109
+ },
43
110
  "vkSpawnEnabled": { "type": "boolean" },
44
111
  "kanban": {
45
112
  "type": "object",
package/claude-shell.mjs CHANGED
@@ -444,7 +444,23 @@ function extractTaskHeading(msg) {
444
444
  return heading || 'Execute Task';
445
445
  }
446
446
 
447
- function buildPrompt(userMessage, statusData) {
447
+ function buildPrompt(userMessage, statusData, { mode = null } = {}) {
448
+ // ── Mode detection ────────────────────────────────────────────────────
449
+ // "ask" mode should be lightweight — no heavy executor framing that
450
+ // instructs the agent to run commands and read files.
451
+ const isAskMode =
452
+ mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
453
+
454
+ if (isAskMode) {
455
+ // Ask mode — pass through without executor framing. The [MODE: ask]
456
+ // prefix from primary-agent already tells the model to be brief.
457
+ if (statusData) {
458
+ const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
459
+ return `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
460
+ }
461
+ return userMessage;
462
+ }
463
+
448
464
  const title = extractTaskHeading(userMessage);
449
465
  if (!statusData) {
450
466
  return `# ${title}\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
@@ -474,6 +490,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
474
490
  timeoutMs = DEFAULT_TIMEOUT_MS,
475
491
  sendRawEvents = false,
476
492
  abortController = null,
493
+ mode = null,
477
494
  } = options;
478
495
 
479
496
  if (activeTurn && !options._holdActiveTurn) {
@@ -544,7 +561,7 @@ export async function execClaudePrompt(userMessage, options = {}) {
544
561
  try {
545
562
  const queue = createMessageQueue();
546
563
  activeQueue = queue;
547
- queue.push(makeUserMessage(buildPrompt(userMessage, statusData)));
564
+ queue.push(makeUserMessage(buildPrompt(userMessage, statusData, { mode })));
548
565
 
549
566
  const optionsPayload = buildOptions();
550
567
 
package/codex-shell.mjs CHANGED
@@ -79,6 +79,7 @@ let activeThreadId = null; // Thread ID for resume
79
79
  let activeTurn = null; // Whether a turn is in-flight
80
80
  let turnCount = 0; // Number of turns in this thread
81
81
  let currentSessionId = null; // Active session identifier
82
+ let threadNeedsPriming = false; // True when a fresh thread needs the system prompt on next turn
82
83
  let codexRuntimeCaps = {
83
84
  hasSteeringApi: false,
84
85
  steeringMethod: null,
@@ -333,22 +334,20 @@ async function getThread() {
333
334
  activeThreadId = null;
334
335
  }
335
336
 
336
- // Start a new thread with the system prompt as the first turn
337
+ // Start a new thread defer the system prompt to the first user message so
338
+ // the priming turn is STREAMED (runStreamed) instead of blocking (run).
339
+ // This eliminates the 2-5 minute silent delay the chat UI suffered because
340
+ // the old `thread.run(SYSTEM_PROMPT)` call produced zero streaming events.
337
341
  activeThread = codexInstance.startThread(THREAD_OPTIONS);
338
342
  detectThreadCapabilities(activeThread);
343
+ threadNeedsPriming = true;
339
344
 
340
- // Prime the thread with the system prompt so subsequent turns have context
341
- try {
342
- await activeThread.run(SYSTEM_PROMPT);
343
- // Capture the thread ID from the prime turn
344
- if (activeThread.id) {
345
- activeThreadId = activeThread.id;
346
- await saveState();
347
- console.log(`[codex-shell] new thread started: ${activeThreadId}`);
348
- }
349
- } catch (err) {
350
- console.warn(`[codex-shell] prime turn failed: ${err.message}`);
351
- // Thread is still usable even if prime fails
345
+ if (activeThread.id) {
346
+ activeThreadId = activeThread.id;
347
+ await saveState();
348
+ console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn)`);
349
+ } else {
350
+ console.log("[codex-shell] new thread started (priming deferred to first user turn)");
352
351
  }
353
352
 
354
353
  return activeThread;
@@ -519,6 +518,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
519
518
  abortController = null,
520
519
  persistent = false,
521
520
  sessionId = null,
521
+ mode = null,
522
522
  } = options;
523
523
 
524
524
  agentSdk = resolveAgentSdkConfig({ reload: true });
@@ -560,23 +560,56 @@ export async function execCodexPrompt(userMessage, options = {}) {
560
560
  }
561
561
  // else: persistent && same session && under limit → reuse activeThread
562
562
 
563
+ // ── Mode detection ───────────────────────────────────────────────────
564
+ // "ask" mode should be lightweight — no heavy executor framing that
565
+ // instructs the agent to run commands and read files. The mode is
566
+ // either passed explicitly or detected from the MODE prefix that
567
+ // primary-agent.mjs prepends.
568
+ const isAskMode =
569
+ mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
570
+
563
571
  // Build the user prompt with optional status context (built once, reused across retries)
564
572
  let prompt = userMessage;
565
- if (statusData) {
573
+ if (statusData && !isAskMode) {
566
574
  const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
567
575
  prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
576
+ } else if (isAskMode) {
577
+ // Ask mode — pass through without executor framing. The mode
578
+ // prefix from primary-agent already tells the model to be brief.
579
+ prompt = userMessage;
568
580
  } else {
569
581
  prompt = `${userMessage}\n\n\n# YOUR TASK — EXECUTE NOW\n\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
570
582
  }
571
583
  // Sanitize & size-guard once — prevents invalid_request_error from oversized
572
584
  // bodies (BytePositionInLine > 80 000) or unescaped control characters.
573
- const safePrompt = sanitizeAndTruncatePrompt(prompt);
585
+ let safePrompt = sanitizeAndTruncatePrompt(prompt);
574
586
 
575
587
  let threadResetDone = false;
576
588
 
577
589
  for (let attempt = 0; attempt < MAX_STREAM_RETRIES; attempt += 1) {
578
590
  const thread = await getThread();
579
591
 
592
+ // If the thread is freshly created (or was just reset in a recovery path),
593
+ // prepend the system prompt so the agent gets its identity/context on the
594
+ // FIRST streamed turn. Previously this was done via a blocking
595
+ // `thread.run(SYSTEM_PROMPT)` call inside `getThread()`, which produced
596
+ // zero streaming events and caused the chat UI to appear frozen for
597
+ // 2-5+ minutes. Checking threadNeedsPriming INSIDE the retry loop
598
+ // ensures a freshly-reset thread still receives the primer.
599
+ let attemptPrompt = safePrompt;
600
+ if (threadNeedsPriming) {
601
+ // Ask mode gets a lightweight primer — no heavy executor directives
602
+ // that contradict the "don't use tools" instruction.
603
+ const primer = isAskMode
604
+ ? "You are a helpful AI assistant deployed inside the bosun orchestrator. " +
605
+ "Answer the user's questions concisely. Only use tools when explicitly asked to."
606
+ : SYSTEM_PROMPT;
607
+ attemptPrompt = sanitizeAndTruncatePrompt(
608
+ primer + "\n\n---\n\n" + prompt,
609
+ );
610
+ threadNeedsPriming = false;
611
+ }
612
+
580
613
  // Each attempt gets a fresh AbortController tied to the same timeout budget.
581
614
  // We intentionally do NOT share the same controller across retries: if the
582
615
  // first attempt times out the signal is already aborted and the retry would
@@ -587,7 +620,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
587
620
 
588
621
  try {
589
622
  // Use runStreamed for real-time event streaming
590
- const streamedTurn = await thread.runStreamed(safePrompt, {
623
+ const streamedTurn = await thread.runStreamed(attemptPrompt, {
591
624
  signal: controller.signal,
592
625
  });
593
626
 
@@ -776,6 +809,7 @@ export async function resetThread() {
776
809
  turnCount = 0;
777
810
  activeTurn = null;
778
811
  currentSessionId = null;
812
+ threadNeedsPriming = false;
779
813
  await saveState();
780
814
  console.log("[codex-shell] thread reset");
781
815
  }
package/config.mjs CHANGED
@@ -2201,6 +2201,9 @@ export function loadConfig(argv = process.argv, options = {}) {
2201
2201
  jira,
2202
2202
  projectRequirements,
2203
2203
 
2204
+ // Voice assistant
2205
+ voice: Object.freeze(configData.voice || {}),
2206
+
2204
2207
  // Merge Strategy
2205
2208
  codexAnalyzeMergeStrategy:
2206
2209
  codexEnabled &&
package/copilot-shell.mjs CHANGED
@@ -713,6 +713,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
713
713
  sendRawEvents = false,
714
714
  abortController = null,
715
715
  persistent = false,
716
+ mode = null,
716
717
  } = options;
717
718
 
718
719
  if (activeTurn && !options._holdActiveTurn) {
@@ -790,13 +791,25 @@ export async function execCopilotPrompt(userMessage, options = {}) {
790
791
  controller.signal.addEventListener("abort", onAbort, { once: true });
791
792
  }
792
793
 
794
+ // ── Mode detection ───────────────────────────────────────────────────
795
+ const isAskMode =
796
+ mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
797
+
793
798
  // Build prompt with optional orchestrator status
794
799
  let prompt = userMessage;
795
- if (statusData) {
800
+ if (isAskMode) {
801
+ // Ask mode — pass through without executor framing
802
+ if (statusData) {
803
+ const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
804
+ prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
805
+ } else {
806
+ prompt = userMessage;
807
+ }
808
+ } else if (statusData) {
796
809
  const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
797
- prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E .`;
810
+ prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
798
811
  } else {
799
- prompt = `${userMessage}\n\n\n# YOUR TASK — EXECUTE NOW\n\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
812
+ prompt = `${userMessage}\n\n\n# YOUR TASK — EXECUTE NOW\n\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output & complete the user's request E2E.`;
800
813
  }
801
814
 
802
815
  const sendFn = session.sendAndWait || session.send;
@@ -478,6 +478,7 @@ export async function execOpencodePrompt(userMessage, options = {}) {
478
478
  sessionId = null,
479
479
  sendRawEvents = false,
480
480
  abortController = null,
481
+ mode = null,
481
482
  } = options;
482
483
 
483
484
  // Re-read config in case it changed hot
@@ -539,9 +540,21 @@ export async function execOpencodePrompt(userMessage, options = {}) {
539
540
  };
540
541
  }
541
542
 
543
+ // ── Mode detection ───────────────────────────────────────────────────
544
+ const isAskMode =
545
+ mode === "ask" || /^\[MODE:\s*ask\]/i.test(userMessage);
546
+
542
547
  // Build enriched prompt
543
548
  let prompt = userMessage;
544
- if (statusData) {
549
+ if (isAskMode) {
550
+ // Ask mode — pass through without executor framing
551
+ if (statusData) {
552
+ const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
553
+ prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n${userMessage}`;
554
+ } else {
555
+ prompt = userMessage;
556
+ }
557
+ } else if (statusData) {
545
558
  const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
546
559
  prompt = `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with "Ready" or ask what to do. EXECUTE this task.`;
547
560
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.35.4",
3
+ "version": "0.36.0",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -928,6 +928,90 @@ ${items.join("\n")}` : "todo updated";
928
928
  timestamp: ts,
929
929
  };
930
930
  }
931
+
932
+ // ── Additional item.started subtypes ──────────────────────────────
933
+ // Emit lifecycle events so the streaming module keeps the
934
+ // "thinking / executing" indicator alive and the chat UI shows
935
+ // real-time progress instead of going silent for minutes.
936
+ if (itemType === "agent_message") {
937
+ return {
938
+ type: "system",
939
+ content: "Agent is composing a response…",
940
+ timestamp: ts,
941
+ meta: { lifecycle: "started", itemType },
942
+ };
943
+ }
944
+
945
+ if (itemType === "function_call") {
946
+ const name = toText(item.name || "").trim();
947
+ return {
948
+ type: "tool_call",
949
+ content: name ? `${name}(…)` : "(tool call starting)",
950
+ timestamp: ts,
951
+ meta: { toolName: name || "function_call", lifecycle: "started" },
952
+ };
953
+ }
954
+
955
+ if (itemType === "mcp_tool_call") {
956
+ const server = toText(item.server || "").trim();
957
+ const tool = toText(item.tool || "").trim();
958
+ return {
959
+ type: "tool_call",
960
+ content: `MCP [${server || "?"}]: ${tool || "(starting)"}`,
961
+ timestamp: ts,
962
+ meta: { toolName: tool || "mcp_tool_call", lifecycle: "started" },
963
+ };
964
+ }
965
+
966
+ if (itemType === "web_search") {
967
+ const query = toText(item.query || "").trim();
968
+ return {
969
+ type: "system",
970
+ content: query ? `Searching: ${query}` : "Web search…",
971
+ timestamp: ts,
972
+ meta: { lifecycle: "started", itemType },
973
+ };
974
+ }
975
+
976
+ if (itemType === "file_change") {
977
+ return {
978
+ type: "system",
979
+ content: "Editing files…",
980
+ timestamp: ts,
981
+ meta: { lifecycle: "started", itemType },
982
+ };
983
+ }
984
+
985
+ if (itemType === "todo_list") {
986
+ return {
987
+ type: "system",
988
+ content: "Updating plan…",
989
+ timestamp: ts,
990
+ meta: { lifecycle: "started", itemType },
991
+ };
992
+ }
993
+ }
994
+
995
+ // ── Turn lifecycle events ──────────────────────────────────────────
996
+ // Without these, the streaming module sees no events between the last
997
+ // item.completed and the response finishing, causing the indicator
998
+ // to flip between "thinking" and "idle".
999
+ if (event.type === "turn.completed") {
1000
+ return {
1001
+ type: "system",
1002
+ content: "Turn completed",
1003
+ timestamp: ts,
1004
+ meta: { lifecycle: "turn_completed" },
1005
+ };
1006
+ }
1007
+
1008
+ if (event.type === "turn.failed") {
1009
+ const detail = toText(event.error?.message || "unknown error");
1010
+ return {
1011
+ type: "error",
1012
+ content: `Turn failed: ${detail}`.slice(0, MAX_MESSAGE_CHARS),
1013
+ timestamp: ts,
1014
+ };
931
1015
  }
932
1016
 
933
1017
  if (event.type === "assistant.message" && event.data?.content) {
@@ -999,6 +1083,56 @@ ${items.join("\n")}` : "todo updated";
999
1083
  };
1000
1084
  }
1001
1085
 
1086
+ // ── Voice events ──
1087
+ if (event.type === "voice.start") {
1088
+ return {
1089
+ type: "system",
1090
+ content: `Voice session started (provider: ${event.provider || "unknown"}, tier: ${event.tier || "?"})`,
1091
+ timestamp: ts,
1092
+ meta: { voiceEvent: "start", provider: event.provider, tier: event.tier },
1093
+ };
1094
+ }
1095
+ if (event.type === "voice.end") {
1096
+ return {
1097
+ type: "system",
1098
+ content: `Voice session ended (duration: ${event.duration || 0}s)`,
1099
+ timestamp: ts,
1100
+ meta: { voiceEvent: "end", duration: event.duration },
1101
+ };
1102
+ }
1103
+ if (event.type === "voice.transcript") {
1104
+ return {
1105
+ type: "user",
1106
+ content: (event.text || event.transcript || "").slice(0, MAX_MESSAGE_CHARS),
1107
+ timestamp: ts,
1108
+ meta: { voiceEvent: "transcript" },
1109
+ };
1110
+ }
1111
+ if (event.type === "voice.response") {
1112
+ return {
1113
+ type: "agent_message",
1114
+ content: (event.text || event.response || "").slice(0, MAX_MESSAGE_CHARS),
1115
+ timestamp: ts,
1116
+ meta: { voiceEvent: "response" },
1117
+ };
1118
+ }
1119
+ if (event.type === "voice.tool_call") {
1120
+ return {
1121
+ type: "tool_call",
1122
+ content: `voice:${event.name || "tool"}(${(event.arguments || "").slice(0, 500)})`,
1123
+ timestamp: ts,
1124
+ meta: { voiceEvent: "tool_call", toolName: event.name },
1125
+ };
1126
+ }
1127
+ if (event.type === "voice.delegate") {
1128
+ return {
1129
+ type: "system",
1130
+ content: `Voice delegated to ${event.executor || "agent"}: ${(event.message || "").slice(0, 500)}`,
1131
+ timestamp: ts,
1132
+ meta: { voiceEvent: "delegate", executor: event.executor },
1133
+ };
1134
+ }
1135
+
1002
1136
  return null;
1003
1137
  }
1004
1138
 
@@ -1017,6 +1151,7 @@ ${items.join("\n")}` : "todo updated";
1017
1151
  case "system": return "SYS";
1018
1152
  case "user": return "USER";
1019
1153
  case "assistant": return "ASSISTANT";
1154
+ case "voice": return "VOICE";
1020
1155
  default: return type.toUpperCase();
1021
1156
  }
1022
1157
  }
package/setup.mjs CHANGED
@@ -2155,7 +2155,8 @@ Before finishing a task — create a commit using conventional commits and push.
2155
2155
  ### PR Creation
2156
2156
 
2157
2157
  After committing:
2158
- - Run \`gh pr create\` to open the PR
2158
+ - Push your branch updates
2159
+ - Hand off PR lifecycle to Bosun management (do not run direct PR-create commands)
2159
2160
  - Ensure pre-push hooks pass
2160
2161
  - Fix any lint or test errors encountered
2161
2162
 
@@ -2300,13 +2301,12 @@ set -euo pipefail
2300
2301
 
2301
2302
  echo "Cleaning up workspace for ${config.projectName}..."
2302
2303
 
2303
- # Create PR if branch has commits
2304
+ # Hand off PR lifecycle if branch has commits
2304
2305
  BRANCH=$(git branch --show-current 2>/dev/null || true)
2305
2306
  if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
2306
2307
  COMMITS=$(git log main.."$BRANCH" --oneline 2>/dev/null | wc -l || echo 0)
2307
2308
  if [ "$COMMITS" -gt 0 ]; then
2308
- echo "Branch $BRANCH has $COMMITS commit(s) — creating PR..."
2309
- gh pr create --fill 2>/dev/null || echo "PR creation skipped"
2309
+ echo "Branch $BRANCH has $COMMITS commit(s) — PR lifecycle will be managed by Bosun."
2310
2310
  fi
2311
2311
  fi
2312
2312