bosun 0.36.2 → 0.36.4

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 (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. package/workspace-reaper.mjs +0 -405
package/autofix.mjs CHANGED
@@ -688,6 +688,7 @@ function detectChangedFiles(repoRoot) {
688
688
  cwd: repoRoot,
689
689
  encoding: "utf8",
690
690
  timeout: 10_000,
691
+ stdio: ["pipe", "pipe", "pipe"],
691
692
  });
692
693
  return output
693
694
  .split(/\r?\n/)
@@ -708,6 +709,7 @@ function getChangeSummary(repoRoot, files) {
708
709
  cwd: repoRoot,
709
710
  encoding: "utf8",
710
711
  timeout: 10_000,
712
+ stdio: ["pipe", "pipe", "pipe"],
711
713
  });
712
714
  return diff.trim() || files.join(", ");
713
715
  } catch {
package/bosun.schema.json CHANGED
@@ -38,7 +38,13 @@
38
38
  "codexEnabled": { "type": "boolean" },
39
39
  "primaryAgent": {
40
40
  "type": "string",
41
- "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"]
41
+ "enum": [
42
+ "codex-sdk",
43
+ "copilot-sdk",
44
+ "claude-sdk",
45
+ "gemini-sdk",
46
+ "opencode-sdk"
47
+ ]
42
48
  },
43
49
  "telegramUiTunnel": {
44
50
  "type": "string",
@@ -147,6 +153,48 @@
147
153
  "default": "auto",
148
154
  "description": "Voice provider: openai/azure (Tier 1 realtime), claude/gemini (Tier 2 voice + provider vision), fallback (browser STT/TTS), auto (detect from env)"
149
155
  },
156
+ "providers": {
157
+ "type": "array",
158
+ "description": "Ordered provider candidates for voice routing/failover. First match with credentials is used.",
159
+ "items": {
160
+ "anyOf": [
161
+ {
162
+ "type": "string",
163
+ "enum": ["openai", "azure", "claude", "gemini", "fallback"]
164
+ },
165
+ {
166
+ "type": "object",
167
+ "additionalProperties": false,
168
+ "properties": {
169
+ "provider": {
170
+ "type": "string",
171
+ "enum": ["openai", "azure", "claude", "gemini", "fallback"]
172
+ },
173
+ "model": { "type": "string" },
174
+ "visionModel": { "type": "string" },
175
+ "voiceId": {
176
+ "type": "string",
177
+ "enum": [
178
+ "alloy",
179
+ "ash",
180
+ "ballad",
181
+ "coral",
182
+ "echo",
183
+ "fable",
184
+ "onyx",
185
+ "nova",
186
+ "sage",
187
+ "shimmer",
188
+ "verse"
189
+ ]
190
+ },
191
+ "azureDeployment": { "type": "string" }
192
+ },
193
+ "required": ["provider"]
194
+ }
195
+ ]
196
+ }
197
+ },
150
198
  "model": {
151
199
  "type": "string",
152
200
  "default": "gpt-4o-realtime-preview-2024-12-17",
@@ -161,10 +209,18 @@
161
209
  "type": "string",
162
210
  "description": "OpenAI API key for Realtime API (overrides OPENAI_API_KEY env)"
163
211
  },
212
+ "openaiAccessToken": {
213
+ "type": "string",
214
+ "description": "OpenAI OAuth access token for voice (OAuth preferred over API key when present)"
215
+ },
164
216
  "azureApiKey": {
165
217
  "type": "string",
166
218
  "description": "Azure OpenAI API key for Realtime API"
167
219
  },
220
+ "azureAccessToken": {
221
+ "type": "string",
222
+ "description": "Azure OAuth/AAD access token for voice realtime"
223
+ },
168
224
  "azureEndpoint": {
169
225
  "type": "string",
170
226
  "description": "Azure OpenAI endpoint URL"
@@ -178,13 +234,33 @@
178
234
  "type": "string",
179
235
  "description": "Anthropic API key for Claude voice/vision provider mode"
180
236
  },
237
+ "claudeAccessToken": {
238
+ "type": "string",
239
+ "description": "Claude OAuth access token for voice provider mode"
240
+ },
181
241
  "geminiApiKey": {
182
242
  "type": "string",
183
243
  "description": "Gemini API key for Gemini voice/vision provider mode"
184
244
  },
245
+ "geminiAccessToken": {
246
+ "type": "string",
247
+ "description": "Gemini OAuth access token for voice provider mode"
248
+ },
185
249
  "voiceId": {
186
250
  "type": "string",
187
- "enum": ["alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"],
251
+ "enum": [
252
+ "alloy",
253
+ "ash",
254
+ "ballad",
255
+ "coral",
256
+ "echo",
257
+ "fable",
258
+ "onyx",
259
+ "nova",
260
+ "sage",
261
+ "shimmer",
262
+ "verse"
263
+ ],
188
264
  "default": "alloy",
189
265
  "description": "Voice ID for TTS output"
190
266
  },
@@ -204,9 +280,31 @@
204
280
  "default": "browser",
205
281
  "description": "Fallback when Realtime API unavailable: browser (Web Speech API) or disabled"
206
282
  },
283
+ "failover": {
284
+ "type": "object",
285
+ "additionalProperties": false,
286
+ "properties": {
287
+ "enabled": {
288
+ "type": "boolean",
289
+ "default": true,
290
+ "description": "Enable automatic realtime failover across configured voice providers"
291
+ },
292
+ "maxAttempts": {
293
+ "type": "number",
294
+ "default": 2,
295
+ "description": "Maximum realtime provider attempts per voice session token request"
296
+ }
297
+ }
298
+ },
207
299
  "delegateExecutor": {
208
300
  "type": "string",
209
- "enum": ["codex-sdk", "copilot-sdk", "claude-sdk", "gemini-sdk", "opencode-sdk"],
301
+ "enum": [
302
+ "codex-sdk",
303
+ "copilot-sdk",
304
+ "claude-sdk",
305
+ "gemini-sdk",
306
+ "opencode-sdk"
307
+ ],
210
308
  "description": "Which executor to use for delegate_to_agent calls. Defaults to primaryAgent."
211
309
  }
212
310
  }
package/codex-shell.mjs CHANGED
@@ -177,6 +177,7 @@ function sanitizeAndTruncatePrompt(text) {
177
177
  return truncated + `\n\n[...prompt truncated — ${removedBytes} bytes removed to stay within API limits]`;
178
178
  }
179
179
  const REPO_ROOT = resolveRepoRoot();
180
+ const DEFAULT_WORKING_DIRECTORY = REPO_ROOT;
180
181
 
181
182
  // ── State ────────────────────────────────────────────────────────────────────
182
183
 
@@ -187,6 +188,7 @@ let activeThreadId = null; // Thread ID for resume
187
188
  let activeTurn = null; // Whether a turn is in-flight
188
189
  let turnCount = 0; // Number of turns in this thread
189
190
  let currentSessionId = null; // Active session identifier
191
+ let activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY; // Session/thread cwd
190
192
  let threadNeedsPriming = false; // True when a fresh thread needs the system prompt on next turn
191
193
  let codexRuntimeCaps = {
192
194
  hasSteeringApi: false,
@@ -200,6 +202,20 @@ function timestamp() {
200
202
  return new Date().toISOString();
201
203
  }
202
204
 
205
+ function normalizeWorkingDirectory(input) {
206
+ const raw = String(input || "").trim();
207
+ if (!raw) return null;
208
+ try {
209
+ return resolve(raw);
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ function getWorkingDirectory() {
216
+ return normalizeWorkingDirectory(activeWorkingDirectory) || DEFAULT_WORKING_DIRECTORY;
217
+ }
218
+
203
219
  function resolveCodexTransport() {
204
220
  const raw = String(process.env.CODEX_TRANSPORT || "auto")
205
221
  .trim()
@@ -250,13 +266,17 @@ async function loadState() {
250
266
  activeThreadId = data.threadId || null;
251
267
  turnCount = data.turnCount || 0;
252
268
  currentSessionId = data.currentSessionId || null;
269
+ activeWorkingDirectory =
270
+ normalizeWorkingDirectory(data.workingDirectory) ||
271
+ DEFAULT_WORKING_DIRECTORY;
253
272
  console.log(
254
- `[codex-shell] loaded state: threadId=${activeThreadId}, turns=${turnCount}, session=${currentSessionId}`,
273
+ `[codex-shell] loaded state: threadId=${activeThreadId}, turns=${turnCount}, session=${currentSessionId}, cwd=${getWorkingDirectory()}`,
255
274
  );
256
275
  } catch {
257
276
  activeThreadId = null;
258
277
  turnCount = 0;
259
278
  currentSessionId = null;
279
+ activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
260
280
  }
261
281
  }
262
282
 
@@ -270,6 +290,7 @@ async function saveState() {
270
290
  threadId: activeThreadId,
271
291
  turnCount,
272
292
  currentSessionId,
293
+ workingDirectory: getWorkingDirectory(),
273
294
  updatedAt: timestamp(),
274
295
  },
275
296
  null,
@@ -311,6 +332,7 @@ async function saveCurrentSession() {
311
332
  await saveSessionData(currentSessionId, {
312
333
  threadId: activeThreadId,
313
334
  turnCount,
335
+ workingDirectory: getWorkingDirectory(),
314
336
  createdAt: (await loadSessionData(currentSessionId))?.createdAt || timestamp(),
315
337
  lastActiveAt: timestamp(),
316
338
  });
@@ -325,12 +347,16 @@ async function loadSession(sessionId) {
325
347
  turnCount = data.turnCount || 0;
326
348
  activeThread = null; // will be re-created/resumed via getThread()
327
349
  currentSessionId = sessionId;
328
- console.log(`[codex-shell] loaded session ${sessionId}: threadId=${activeThreadId}, turns=${turnCount}`);
350
+ activeWorkingDirectory =
351
+ normalizeWorkingDirectory(data.workingDirectory) ||
352
+ DEFAULT_WORKING_DIRECTORY;
353
+ console.log(`[codex-shell] loaded session ${sessionId}: threadId=${activeThreadId}, turns=${turnCount}, cwd=${getWorkingDirectory()}`);
329
354
  } else {
330
355
  activeThread = null;
331
356
  activeThreadId = null;
332
357
  turnCount = 0;
333
358
  currentSessionId = sessionId;
359
+ activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
334
360
  console.log(`[codex-shell] created new session ${sessionId}`);
335
361
  }
336
362
  await saveState();
@@ -366,9 +392,8 @@ Key files:
366
392
  AGENTS.md — Repo guide for agents
367
393
  `;
368
394
 
369
- const THREAD_OPTIONS = {
395
+ const THREAD_BASE_OPTIONS = {
370
396
  sandboxMode: process.env.CODEX_SANDBOX || "workspace-write",
371
- workingDirectory: REPO_ROOT,
372
397
  skipGitRepoCheck: true,
373
398
  webSearchMode: "live",
374
399
  approvalPolicy: "never",
@@ -377,6 +402,13 @@ const THREAD_OPTIONS = {
377
402
  // codex-config.mjs ensureFeatureFlags() handles this during setup.
378
403
  };
379
404
 
405
+ function buildThreadOptions() {
406
+ return {
407
+ ...THREAD_BASE_OPTIONS,
408
+ workingDirectory: getWorkingDirectory(),
409
+ };
410
+ }
411
+
380
412
  /**
381
413
  * Get or create a thread.
382
414
  * Uses fresh-thread mode by default to avoid context bloat.
@@ -384,6 +416,7 @@ const THREAD_OPTIONS = {
384
416
  */
385
417
  async function getThread() {
386
418
  if (activeThread) return activeThread;
419
+ const threadOptions = buildThreadOptions();
387
420
 
388
421
  const { env: resolvedEnv } = resolveCodexProfileRuntime(process.env);
389
422
  Object.assign(process.env, resolvedEnv);
@@ -391,8 +424,21 @@ async function getThread() {
391
424
  if (!codexInstance) {
392
425
  const Cls = await loadCodexSdk();
393
426
  if (!Cls) throw new Error("Codex SDK not available");
394
- // Pass feature overrides via --config so they apply even if config.toml
395
- // hasn't been patched by codex-config.mjs yet.
427
+
428
+ // Inject stream resilience settings via --config overrides so they apply
429
+ // even if config.toml hasn't been patched by codex-config.mjs yet.
430
+ // This is the most reliable path for Azure/Foundry deployments where
431
+ // dropped SSE streams ("response.failed") are the dominant failure mode.
432
+ const providerName = resolvedEnv.OPENAI_BASE_URL?.toLowerCase().includes(".openai.azure.com")
433
+ ? "azure"
434
+ : "openai";
435
+ const STREAM_IDLE_TIMEOUT_MS = 3_600_000; // 60 min — matches Azure max stream lifetime
436
+ const streamProviderOverrides = {
437
+ stream_idle_timeout_ms: STREAM_IDLE_TIMEOUT_MS,
438
+ stream_max_retries: 15,
439
+ request_max_retries: 6,
440
+ };
441
+
396
442
  codexInstance = new Cls({
397
443
  config: {
398
444
  features: {
@@ -402,8 +448,13 @@ async function getThread() {
402
448
  undo: true,
403
449
  steer: true,
404
450
  },
451
+ model_providers: {
452
+ [providerName]: streamProviderOverrides,
453
+ },
405
454
  },
406
455
  });
456
+
457
+ console.log(`[codex-shell] created Codex instance (provider=${providerName}, stream_idle_timeout=${STREAM_IDLE_TIMEOUT_MS}ms, stream_max_retries=${streamProviderOverrides.stream_max_retries})`);
407
458
  }
408
459
 
409
460
  const transport = resolveCodexTransport();
@@ -414,7 +465,7 @@ async function getThread() {
414
465
  try {
415
466
  activeThread = codexInstance.resumeThread(
416
467
  activeThreadId,
417
- THREAD_OPTIONS,
468
+ threadOptions,
418
469
  );
419
470
  if (activeThread) {
420
471
  detectThreadCapabilities(activeThread);
@@ -446,16 +497,16 @@ async function getThread() {
446
497
  // the priming turn is STREAMED (runStreamed) instead of blocking (run).
447
498
  // This eliminates the 2-5 minute silent delay the chat UI suffered because
448
499
  // the old `thread.run(SYSTEM_PROMPT)` call produced zero streaming events.
449
- activeThread = codexInstance.startThread(THREAD_OPTIONS);
500
+ activeThread = codexInstance.startThread(threadOptions);
450
501
  detectThreadCapabilities(activeThread);
451
502
  threadNeedsPriming = true;
452
503
 
453
504
  if (activeThread.id) {
454
505
  activeThreadId = activeThread.id;
455
506
  await saveState();
456
- console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn)`);
507
+ console.log(`[codex-shell] new thread started: ${activeThreadId} (priming deferred to first user turn, cwd=${threadOptions.workingDirectory})`);
457
508
  } else {
458
- console.log("[codex-shell] new thread started (priming deferred to first user turn)");
509
+ console.log(`[codex-shell] new thread started (priming deferred to first user turn, cwd=${threadOptions.workingDirectory})`);
459
510
  }
460
511
 
461
512
  return activeThread;
@@ -627,6 +678,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
627
678
  persistent = false,
628
679
  sessionId = null,
629
680
  mode = null,
681
+ cwd = null,
630
682
  } = options;
631
683
 
632
684
  agentSdk = resolveAgentSdkConfig({ reload: true });
@@ -651,9 +703,13 @@ export async function execCodexPrompt(userMessage, options = {}) {
651
703
 
652
704
  try {
653
705
  const streamSafety = resolveCodexStreamSafety(timeoutMs);
706
+ const requestedWorkingDirectory = normalizeWorkingDirectory(cwd);
707
+
654
708
  if (!persistent) {
655
709
  // Task executor path — keep existing fresh-thread behavior
656
710
  activeThread = null;
711
+ activeWorkingDirectory =
712
+ requestedWorkingDirectory || DEFAULT_WORKING_DIRECTORY;
657
713
  } else if (sessionId && sessionId !== currentSessionId) {
658
714
  // Switching to a different persistent session
659
715
  await loadSession(sessionId);
@@ -669,6 +725,24 @@ export async function execCodexPrompt(userMessage, options = {}) {
669
725
  }
670
726
  // else: persistent && same session && under limit → reuse activeThread
671
727
 
728
+ if (
729
+ requestedWorkingDirectory &&
730
+ requestedWorkingDirectory !== getWorkingDirectory()
731
+ ) {
732
+ activeWorkingDirectory = requestedWorkingDirectory;
733
+ activeThread = null;
734
+ activeThreadId = null;
735
+ turnCount = 0;
736
+ threadNeedsPriming = false;
737
+ await saveState();
738
+ if (persistent && currentSessionId) {
739
+ await saveCurrentSession();
740
+ }
741
+ console.log(
742
+ `[codex-shell] switched working directory to ${requestedWorkingDirectory} for session ${currentSessionId || "(ephemeral)"}`,
743
+ );
744
+ }
745
+
672
746
  // ── Mode detection ───────────────────────────────────────────────────
673
747
  // "ask" mode should be lightweight — no heavy executor framing that
674
748
  // instructs the agent to run commands and read files. The mode is
@@ -960,6 +1034,7 @@ export async function resetThread() {
960
1034
  turnCount = 0;
961
1035
  activeTurn = null;
962
1036
  currentSessionId = null;
1037
+ activeWorkingDirectory = DEFAULT_WORKING_DIRECTORY;
963
1038
  threadNeedsPriming = false;
964
1039
  await saveState();
965
1040
  console.log("[codex-shell] thread reset");