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/codex-config.mjs CHANGED
@@ -1569,46 +1569,46 @@ export function ensureCodexConfig({
1569
1569
  */
1570
1570
  export function printConfigSummary(result, log = console.log) {
1571
1571
  if (result.noChanges) {
1572
- log(" Codex CLI config is already up to date");
1572
+ log(" :check: Codex CLI config is already up to date");
1573
1573
  log(` ${result.path}`);
1574
1574
  return;
1575
1575
  }
1576
1576
 
1577
1577
  if (result.created) {
1578
- log(" 📝 Created new Codex CLI config");
1578
+ log(" :edit: Created new Codex CLI config");
1579
1579
  }
1580
1580
 
1581
1581
  if (result.vkAdded) {
1582
- log(" Added Vibe-Kanban MCP server to Codex config");
1582
+ log(" :check: Added Vibe-Kanban MCP server to Codex config");
1583
1583
  }
1584
1584
 
1585
1585
  if (result.vkRemoved) {
1586
- log(" 🗑️ Removed Vibe-Kanban MCP server from global config (workspace-scoped only)");
1586
+ log(" :trash: Removed Vibe-Kanban MCP server from global config (workspace-scoped only)");
1587
1587
  }
1588
1588
 
1589
1589
  if (result.vkEnvUpdated) {
1590
- log(" Updated Vibe-Kanban MCP environment variables");
1590
+ log(" :check: Updated Vibe-Kanban MCP environment variables");
1591
1591
  }
1592
1592
 
1593
1593
  if (result.agentSdkAdded) {
1594
- log(" Added agent SDK selection block");
1594
+ log(" :check: Added agent SDK selection block");
1595
1595
  }
1596
1596
 
1597
1597
  if (result.featuresAdded && result.featuresAdded.length > 0) {
1598
1598
  const key = result.featuresAdded.length <= 5
1599
1599
  ? result.featuresAdded.join(", ")
1600
1600
  : `${result.featuresAdded.length} feature flags`;
1601
- log(` Added feature flags: ${key}`);
1601
+ log(` :check: Added feature flags: ${key}`);
1602
1602
  }
1603
1603
 
1604
1604
  if (result.sandboxAdded) {
1605
- log(" Added sandbox permissions (disk-full-write-access)");
1605
+ log(" :check: Added sandbox permissions (disk-full-write-access)");
1606
1606
  }
1607
1607
 
1608
1608
  if (result.sandboxWorkspaceAdded) {
1609
- log(" Added sandbox workspace-write defaults");
1609
+ log(" :check: Added sandbox workspace-write defaults");
1610
1610
  } else if (result.sandboxWorkspaceUpdated) {
1611
- log(" Updated sandbox workspace-write defaults");
1611
+ log(" :check: Updated sandbox workspace-write defaults");
1612
1612
  }
1613
1613
 
1614
1614
  if (result.sandboxWorkspaceRootsAdded && result.sandboxWorkspaceRootsAdded.length > 0) {
@@ -1619,7 +1619,7 @@ export function printConfigSummary(result, log = console.log) {
1619
1619
 
1620
1620
  if (result.sandboxStaleRootsRemoved && result.sandboxStaleRootsRemoved.length > 0) {
1621
1621
  log(
1622
- ` 🗑️ Pruned ${result.sandboxStaleRootsRemoved.length} stale writable root(s) that no longer exist`,
1622
+ ` :trash: Pruned ${result.sandboxStaleRootsRemoved.length} stale writable root(s) that no longer exist`,
1623
1623
  );
1624
1624
  for (const r of result.sandboxStaleRootsRemoved) {
1625
1625
  log(` - ${r}`);
@@ -1627,7 +1627,7 @@ export function printConfigSummary(result, log = console.log) {
1627
1627
  }
1628
1628
 
1629
1629
  if (result.shellEnvAdded) {
1630
- log(" Added shell environment policy (inherit=all)");
1630
+ log(" :check: Added shell environment policy (inherit=all)");
1631
1631
  }
1632
1632
 
1633
1633
  if (result.agentMaxThreads) {
@@ -1637,22 +1637,22 @@ export function printConfigSummary(result, log = console.log) {
1637
1637
  : String(result.agentMaxThreads.from);
1638
1638
  const toLabel = String(result.agentMaxThreads.to);
1639
1639
  const note = result.agentMaxThreads.explicit ? " (env override)" : "";
1640
- log(` Set agents.max_threads: ${fromLabel} → ${toLabel}${note}`);
1640
+ log(` :check: Set agents.max_threads: ${fromLabel} → ${toLabel}${note}`);
1641
1641
  } else if (result.agentMaxThreadsSkipped) {
1642
1642
  log(
1643
- ` Skipped agents.max_threads (invalid value: ${result.agentMaxThreadsSkipped})`,
1643
+ ` :alert: Skipped agents.max_threads (invalid value: ${result.agentMaxThreadsSkipped})`,
1644
1644
  );
1645
1645
  }
1646
1646
 
1647
1647
  if (result.commonMcpAdded) {
1648
1648
  log(
1649
- " Added common MCP servers (context7, sequential-thinking, playwright, microsoft-docs)",
1649
+ " :check: Added common MCP servers (context7, sequential-thinking, playwright, microsoft-docs)",
1650
1650
  );
1651
1651
  }
1652
1652
 
1653
1653
  if (result.profileProvidersAdded && result.profileProvidersAdded.length > 0) {
1654
1654
  log(
1655
- ` Added model provider sections: ${result.profileProvidersAdded.join(", ")}`,
1655
+ ` :check: Added model provider sections: ${result.profileProvidersAdded.join(", ")}`,
1656
1656
  );
1657
1657
  }
1658
1658
 
@@ -1661,12 +1661,12 @@ export function printConfigSummary(result, log = console.log) {
1661
1661
  t.from === null ? "not set" : `${(t.from / 1000).toFixed(0)}s`;
1662
1662
  const toLabel = `${(t.to / 1000 / 60).toFixed(0)} min`;
1663
1663
  log(
1664
- ` Set stream_idle_timeout_ms on [${t.provider}]: ${fromLabel} → ${toLabel}`,
1664
+ ` :check: Set stream_idle_timeout_ms on [${t.provider}]: ${fromLabel} → ${toLabel}`,
1665
1665
  );
1666
1666
  }
1667
1667
 
1668
1668
  for (const p of result.retriesAdded) {
1669
- log(` Added retry settings to [${p}]`);
1669
+ log(` :check: Added retry settings to [${p}]`);
1670
1670
  }
1671
1671
 
1672
1672
  log(` Config: ${result.path}`);
@@ -1756,7 +1756,7 @@ function parseTomlArrayLiteralEscaped(raw) {
1756
1756
  *
1757
1757
  * Codex refuses to load a per-project .codex/config.toml unless the project
1758
1758
  * directory appears in this list — producing warnings like:
1759
- * " Project config.toml files are disabled … add <dir> as a trusted project"
1759
+ * ":alert: Project config.toml files are disabled … add <dir> as a trusted project"
1760
1760
  *
1761
1761
  * Paths are stored as-is (forward or back slashes preserved) with proper TOML
1762
1762
  * escaping so Windows paths survive round-trips through the file.
package/codex-shell.mjs CHANGED
@@ -16,6 +16,7 @@ import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
16
16
  import { resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { resolveAgentSdkConfig } from "./agent-sdk.mjs";
19
+ import { loadConfig } from "./config.mjs";
19
20
  import { resolveRepoRoot } from "./repo-root.mjs";
20
21
  import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
21
22
  import {
@@ -39,6 +40,113 @@ const MAX_PERSISTENT_TURNS = 50;
39
40
  // 180 KB is a safe ceiling; the API hard-errors around 200–400 KB payloads
40
41
  // that contain embedded content with unescaped characters.
41
42
  const MAX_PROMPT_BYTES = 180_000;
43
+ const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 120_000;
44
+ const DEFAULT_MAX_ITEMS_PER_TURN = 600;
45
+ const DEFAULT_MAX_ITEM_CHARS = 12_000;
46
+ const TOOL_OUTPUT_GUARDRAIL = String.raw`
47
+
48
+ [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.`;
49
+
50
+ function parseBoundedNumber(value, fallback, min, max) {
51
+ const num = Number(value);
52
+ if (!Number.isFinite(num)) return fallback;
53
+ return Math.min(Math.max(Math.trunc(num), min), max);
54
+ }
55
+
56
+ function getInternalExecutorStreamConfig() {
57
+ try {
58
+ const cfg = loadConfig();
59
+ const stream = cfg?.internalExecutor?.stream;
60
+ return stream && typeof stream === "object" ? stream : {};
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ function truncateText(text, maxChars) {
67
+ if (typeof text !== "string") return text;
68
+ if (!Number.isFinite(maxChars) || maxChars < 1 || text.length <= maxChars) {
69
+ return text;
70
+ }
71
+ const trimmed = text.slice(0, maxChars);
72
+ const removed = text.length - maxChars;
73
+ return `${trimmed}
74
+
75
+ […truncated ${removed} chars…]`;
76
+ }
77
+
78
+ function truncateItemForStorage(item, maxChars) {
79
+ if (!item || typeof item !== "object") return item;
80
+ if (!Number.isFinite(maxChars) || maxChars < 1) return item;
81
+
82
+ const next = { ...item };
83
+ const directStringKeys = [
84
+ "text",
85
+ "output",
86
+ "aggregated_output",
87
+ "stderr",
88
+ "stdout",
89
+ "result",
90
+ "message",
91
+ ];
92
+ for (const key of directStringKeys) {
93
+ if (typeof next[key] === "string") {
94
+ next[key] = truncateText(next[key], maxChars);
95
+ }
96
+ }
97
+
98
+ if (Array.isArray(next.content)) {
99
+ next.content = next.content.map((entry) => {
100
+ if (entry && typeof entry === "object" && typeof entry.text === "string") {
101
+ return { ...entry, text: truncateText(entry.text, maxChars) };
102
+ }
103
+ return entry;
104
+ });
105
+ }
106
+
107
+ if (next.error && typeof next.error === "object") {
108
+ next.error = {
109
+ ...next.error,
110
+ message: truncateText(next.error.message, maxChars),
111
+ };
112
+ }
113
+
114
+ return next;
115
+ }
116
+
117
+ function resolveCodexStreamSafety(totalTimeoutMs) {
118
+ const streamCfg = getInternalExecutorStreamConfig();
119
+ const firstEventRaw =
120
+ process.env.INTERNAL_EXECUTOR_STREAM_FIRST_EVENT_TIMEOUT_MS ||
121
+ streamCfg.firstEventTimeoutMs ||
122
+ DEFAULT_FIRST_EVENT_TIMEOUT_MS;
123
+ const maxItemsRaw =
124
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN ||
125
+ streamCfg.maxItemsPerTurn ||
126
+ DEFAULT_MAX_ITEMS_PER_TURN;
127
+ const maxItemCharsRaw =
128
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEM_CHARS ||
129
+ streamCfg.maxItemChars ||
130
+ DEFAULT_MAX_ITEM_CHARS;
131
+ const configuredFirstEventMs = parseBoundedNumber(
132
+ firstEventRaw,
133
+ DEFAULT_FIRST_EVENT_TIMEOUT_MS,
134
+ 1_000,
135
+ 60 * 60 * 1000,
136
+ );
137
+ const budgetMs = Number(totalTimeoutMs);
138
+ let firstEventTimeoutMs = null;
139
+ if (Number.isFinite(budgetMs) && budgetMs > 2_000) {
140
+ const maxAllowed = Math.max(1_000, budgetMs - 1_000);
141
+ firstEventTimeoutMs = Math.min(configuredFirstEventMs, maxAllowed);
142
+ }
143
+
144
+ return {
145
+ firstEventTimeoutMs,
146
+ maxItemsPerTurn: parseBoundedNumber(maxItemsRaw, DEFAULT_MAX_ITEMS_PER_TURN, 1, 5000),
147
+ maxItemChars: parseBoundedNumber(maxItemCharsRaw, DEFAULT_MAX_ITEM_CHARS, 1, 250000),
148
+ };
149
+ }
42
150
 
43
151
  /**
44
152
  * Strip ASCII control characters (except \n/\t) that corrupt JSON serialization,
@@ -380,25 +488,25 @@ function formatEvent(event) {
380
488
  const item = event.item;
381
489
  switch (item.type) {
382
490
  case "command_execution":
383
- return `⚡ Running: \`${item.command}\``;
491
+ return `:zap: Running: \`${item.command}\``;
384
492
  case "file_change":
385
493
  return null; // wait for completed
386
494
  case "mcp_tool_call":
387
- return `🔌 MCP [${item.server}]: ${item.tool}`;
495
+ return `:plug: MCP [${item.server}]: ${item.tool}`;
388
496
  case "reasoning":
389
- return item.text ? `💭 ${item.text.slice(0, 300)}` : null;
497
+ return item.text ? `:u1f4ad: ${item.text.slice(0, 300)}` : null;
390
498
  case "agent_message":
391
499
  return null; // wait for completed for full text
392
500
  case "todo_list":
393
501
  if (item.items && item.items.length > 0) {
394
502
  const todoLines = item.items.map(
395
- (t) => ` ${t.completed ? "" : ""} ${t.text}`,
503
+ (t) => ` ${t.completed ? ":check:" : ":dot:"} ${t.text}`,
396
504
  );
397
- return `📋 Plan:\n${todoLines.join("\n")}`;
505
+ return `:clipboard: Plan:\n${todoLines.join("\n")}`;
398
506
  }
399
507
  return null;
400
508
  case "web_search":
401
- return `🔍 Searching: ${item.query}`;
509
+ return `:search: Searching: ${item.query}`;
402
510
  default:
403
511
  return null;
404
512
  }
@@ -408,7 +516,7 @@ function formatEvent(event) {
408
516
  const item = event.item;
409
517
  switch (item.type) {
410
518
  case "command_execution": {
411
- const status = item.exit_code === 0 ? "" : "";
519
+ const status = item.exit_code === 0 ? ":check:" : ":close:";
412
520
  const output = item.aggregated_output
413
521
  ? `\n${item.aggregated_output.slice(-500)}`
414
522
  : "";
@@ -418,16 +526,16 @@ function formatEvent(event) {
418
526
  if (item.changes && item.changes.length > 0) {
419
527
  const fileLines = item.changes.map(
420
528
  (c) =>
421
- ` ${c.kind === "add" ? "" : c.kind === "delete" ? "🗑️" : "✏️"} ${c.path}`,
529
+ ` ${c.kind === "add" ? ":plus:" : c.kind === "delete" ? ":trash:" : ":edit:"} ${c.path}`,
422
530
  );
423
- return `📁 Files changed:\n${fileLines.join("\n")}`;
531
+ return `:folder: Files changed:\n${fileLines.join("\n")}`;
424
532
  }
425
533
  return null;
426
534
  }
427
535
  case "agent_message":
428
536
  return item.text || null;
429
537
  case "mcp_tool_call": {
430
- const status = item.status === "completed" ? "" : "";
538
+ const status = item.status === "completed" ? ":check:" : ":close:";
431
539
  const resultInfo = item.error
432
540
  ? `Error: ${item.error.message}`
433
541
  : "done";
@@ -436,9 +544,9 @@ function formatEvent(event) {
436
544
  case "todo_list": {
437
545
  if (item.items && item.items.length > 0) {
438
546
  const todoLines = item.items.map(
439
- (t) => ` ${t.completed ? "" : ""} ${t.text}`,
547
+ (t) => ` ${t.completed ? ":check:" : ":dot:"} ${t.text}`,
440
548
  );
441
- return `📋 Updated plan:\n${todoLines.join("\n")}`;
549
+ return `:clipboard: Updated plan:\n${todoLines.join("\n")}`;
442
550
  }
443
551
  return null;
444
552
  }
@@ -451,13 +559,13 @@ function formatEvent(event) {
451
559
  const item = event.item;
452
560
  // Stream partial reasoning and command output
453
561
  if (item.type === "reasoning" && item.text) {
454
- return `💭 ${item.text.slice(0, 300)}`;
562
+ return `:u1f4ad: ${item.text.slice(0, 300)}`;
455
563
  }
456
564
  if (item.type === "todo_list" && item.items) {
457
565
  const todoLines = item.items.map(
458
- (t) => ` ${t.completed ? "" : ""} ${t.text}`,
566
+ (t) => ` ${t.completed ? ":check:" : ":dot:"} ${t.text}`,
459
567
  );
460
- return `📋 Plan update:\n${todoLines.join("\n")}`;
568
+ return `:clipboard: Plan update:\n${todoLines.join("\n")}`;
461
569
  }
462
570
  return null;
463
571
  }
@@ -465,9 +573,9 @@ function formatEvent(event) {
465
573
  case "turn.completed":
466
574
  return null; // handled by caller
467
575
  case "turn.failed":
468
- return `❌ Turn failed: ${event.error?.message || "unknown error"}`;
576
+ return `:close: Turn failed: ${event.error?.message || "unknown error"}`;
469
577
  case "error":
470
- return `❌ Error: ${event.message}`;
578
+ return `:close: Error: ${event.message}`;
471
579
  default:
472
580
  return null;
473
581
  }
@@ -524,7 +632,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
524
632
  agentSdk = resolveAgentSdkConfig({ reload: true });
525
633
  if (agentSdk.primary !== "codex") {
526
634
  return {
527
- finalResponse: `❌ Agent SDK set to "${agentSdk.primary}" — Codex SDK disabled.`,
635
+ finalResponse: `:close: Agent SDK set to "${agentSdk.primary}" — Codex SDK disabled.`,
528
636
  items: [],
529
637
  usage: null,
530
638
  };
@@ -533,7 +641,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
533
641
  if (activeTurn) {
534
642
  return {
535
643
  finalResponse:
536
- " Agent is still executing a previous task. Please wait.",
644
+ ":clock: Agent is still executing a previous task. Please wait.",
537
645
  items: [],
538
646
  usage: null,
539
647
  };
@@ -542,6 +650,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
542
650
  activeTurn = true;
543
651
 
544
652
  try {
653
+ const streamSafety = resolveCodexStreamSafety(timeoutMs);
545
654
  if (!persistent) {
546
655
  // Task executor path — keep existing fresh-thread behavior
547
656
  activeThread = null;
@@ -572,13 +681,13 @@ export async function execCodexPrompt(userMessage, options = {}) {
572
681
  let prompt = userMessage;
573
682
  if (statusData && !isAskMode) {
574
683
  const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
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.`;
684
+ 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.${TOOL_OUTPUT_GUARDRAIL}`;
576
685
  } else if (isAskMode) {
577
686
  // Ask mode — pass through without executor framing. The mode
578
687
  // prefix from primary-agent already tells the model to be brief.
579
688
  prompt = userMessage;
580
689
  } else {
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.`;
690
+ 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.${TOOL_OUTPUT_GUARDRAIL}`;
582
691
  }
583
692
  // Sanitize & size-guard once — prevents invalid_request_error from oversized
584
693
  // bodies (BytePositionInLine > 80 000) or unescaped control characters.
@@ -627,9 +736,27 @@ export async function execCodexPrompt(userMessage, options = {}) {
627
736
  let finalResponse = "";
628
737
  const allItems = [];
629
738
  let turnFailedErr = null;
739
+ let firstEventTimer = null;
740
+ let eventCount = 0;
741
+ let droppedItems = 0;
630
742
 
631
743
  // Process events from the async generator
744
+ if (streamSafety.firstEventTimeoutMs) {
745
+ firstEventTimer = setTimeout(() => {
746
+ if (eventCount > 0 || controller.signal.aborted) return;
747
+ controller.abort("first_event_timeout");
748
+ }, streamSafety.firstEventTimeoutMs);
749
+ if (typeof firstEventTimer.unref === "function") {
750
+ firstEventTimer.unref();
751
+ }
752
+ }
753
+
632
754
  for await (const event of streamedTurn.events) {
755
+ eventCount += 1;
756
+ if (firstEventTimer) {
757
+ clearTimeout(firstEventTimer);
758
+ firstEventTimer = null;
759
+ }
633
760
  // Capture thread ID on first turn
634
761
  if (event.type === "thread.started" && event.thread_id) {
635
762
  activeThreadId = event.thread_id;
@@ -661,7 +788,13 @@ export async function execCodexPrompt(userMessage, options = {}) {
661
788
 
662
789
  // Collect items
663
790
  if (event.type === "item.completed") {
664
- allItems.push(event.item);
791
+ if (allItems.length < streamSafety.maxItemsPerTurn) {
792
+ allItems.push(
793
+ truncateItemForStorage(event.item, streamSafety.maxItemChars),
794
+ );
795
+ } else {
796
+ droppedItems += 1;
797
+ }
665
798
  if (event.item.type === "agent_message" && event.item.text) {
666
799
  finalResponse += event.item.text + "\n";
667
800
  }
@@ -677,6 +810,18 @@ export async function execCodexPrompt(userMessage, options = {}) {
677
810
  }
678
811
  }
679
812
 
813
+ if (firstEventTimer) {
814
+ clearTimeout(firstEventTimer);
815
+ firstEventTimer = null;
816
+ }
817
+
818
+ if (droppedItems > 0) {
819
+ allItems.push({
820
+ type: "stream_notice",
821
+ text: `Dropped ${droppedItems} completed items to stay within INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN=${streamSafety.maxItemsPerTurn}.`,
822
+ });
823
+ }
824
+
680
825
  // If a turn.failed event was seen during the stream, treat it as a
681
826
  // transient stream error so the retry loop handles it correctly.
682
827
  if (turnFailedErr) throw turnFailedErr;
@@ -694,11 +839,17 @@ export async function execCodexPrompt(userMessage, options = {}) {
694
839
 
695
840
  if (err.name === "AbortError") {
696
841
  const reason = controller.signal.reason;
697
- const msg =
698
- reason === "user_stop"
699
- ? "🛑 Agent stopped by user."
700
- : `⏱️ Agent timed out after ${timeoutMs / 1000}s`;
701
- return { finalResponse: msg, items: [], usage: null };
842
+ if (reason === "first_event_timeout") {
843
+ err = new Error(
844
+ `stream disconnected before completion: no stream events within ${streamSafety.firstEventTimeoutMs}ms`,
845
+ );
846
+ } else {
847
+ const msg =
848
+ reason === "user_stop"
849
+ ? ":close: Agent stopped by user."
850
+ : `:clock: Agent timed out after ${timeoutMs / 1000}s`;
851
+ return { finalResponse: msg, items: [], usage: null };
852
+ }
702
853
  }
703
854
 
704
855
  // ── Thread corruption errors: reset thread & retry once ──────────────
@@ -727,7 +878,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
727
878
  `[codex-shell] stream disconnection not resolved after ${MAX_STREAM_RETRIES} attempts — giving up`,
728
879
  );
729
880
  return {
730
- finalResponse: `❌ Stream disconnected after ${MAX_STREAM_RETRIES} retries: ${err.message}`,
881
+ finalResponse: `:close: Stream disconnected after ${MAX_STREAM_RETRIES} retries: ${err.message}`,
731
882
  items: [],
732
883
  usage: null,
733
884
  };
@@ -737,7 +888,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
737
888
  }
738
889
  }
739
890
  return {
740
- finalResponse: " Agent failed after all retry attempts.",
891
+ finalResponse: ":close: Agent failed after all retry attempts.",
741
892
  items: [],
742
893
  usage: null,
743
894
  };
package/config-doctor.mjs CHANGED
@@ -399,6 +399,8 @@ export function runConfigDoctor(options = {}) {
399
399
  const needsClaude =
400
400
  executorsList.includes("claude") || executorsList.includes("anthropic");
401
401
  const needsCopilot = executorsList.includes("copilot");
402
+ const needsGemini = executorsList.includes("gemini");
403
+ const needsOpencode = executorsList.includes("opencode");
402
404
 
403
405
  if (needsOpenAI) {
404
406
  const openaiKey =
@@ -461,10 +463,33 @@ export function runConfigDoctor(options = {}) {
461
463
  }
462
464
  }
463
465
 
466
+ if (needsGemini) {
467
+ const geminiKey =
468
+ effective.GEMINI_API_KEY || effective.GOOGLE_API_KEY || "";
469
+ if (!geminiKey) {
470
+ issues.errors.push({
471
+ code: "GEMINI_API_KEY_MISSING",
472
+ message:
473
+ "EXECUTORS uses gemini but GEMINI_API_KEY/GOOGLE_API_KEY is not set.",
474
+ fix: "Set GEMINI_API_KEY or GOOGLE_API_KEY in your .env",
475
+ });
476
+ }
477
+ }
478
+
479
+ if (needsOpencode && !commandExists("opencode")) {
480
+ issues.warnings.push({
481
+ code: "OPENCODE_BINARY_MISSING",
482
+ message:
483
+ "EXECUTORS uses opencode but the 'opencode' binary is not on PATH.",
484
+ fix: "Install OpenCode CLI/server or remove OPENCODE from EXECUTORS.",
485
+ });
486
+ }
487
+
464
488
  // ── Model Name Validation ─────────────────────────────────────────
465
489
  const modelVars = [
466
490
  "COPILOT_MODEL",
467
491
  "CLAUDE_MODEL",
492
+ "GEMINI_MODEL",
468
493
  "OPENAI_MODEL",
469
494
  "CODEX_MODEL",
470
495
  ];
@@ -942,7 +967,7 @@ export function formatWorkspaceHealthReport(result) {
942
967
  if (result.issues.warnings.length > 0) {
943
968
  lines.push("Warnings:");
944
969
  for (const w of result.issues.warnings) {
945
- lines.push(` ${w.message}`);
970
+ lines.push(` :alert: ${w.message}`);
946
971
  if (w.fix) lines.push(` fix: ${w.fix}`);
947
972
  }
948
973
  lines.push("");
@@ -950,7 +975,7 @@ export function formatWorkspaceHealthReport(result) {
950
975
  if (result.issues.infos.length > 0) {
951
976
  lines.push("Info:");
952
977
  for (const i of result.issues.infos) {
953
- lines.push(` ${i.message}`);
978
+ lines.push(` :help: ${i.message}`);
954
979
  }
955
980
  lines.push("");
956
981
  }
package/config.mjs CHANGED
@@ -857,6 +857,10 @@ function normalizePrimaryAgent(value) {
857
857
  return "copilot-sdk";
858
858
  if (["claude", "claude-sdk", "claude_code", "claude-code"].includes(raw))
859
859
  return "claude-sdk";
860
+ if (["gemini", "gemini-sdk", "google-gemini"].includes(raw))
861
+ return "gemini-sdk";
862
+ if (["opencode", "opencode-sdk", "open-code"].includes(raw))
863
+ return "opencode-sdk";
860
864
  return raw;
861
865
  }
862
866
 
@@ -1266,19 +1270,28 @@ function loadWorkspaceRepoConfig(configDir, configData = {}, activeWorkspace = "
1266
1270
 
1267
1271
  return targetWorkspace.repos
1268
1272
  .map((repo, index) => {
1269
- if (!repo || typeof repo !== "object") return null;
1270
- const name = String(repo.name || repo.id || "").trim();
1273
+ const rawRepo =
1274
+ typeof repo === "string"
1275
+ ? { slug: repo }
1276
+ : (repo && typeof repo === "object" ? repo : null);
1277
+ if (!rawRepo) return null;
1278
+ const slug = String(rawRepo.slug || "").trim();
1279
+ const name = String(rawRepo.name || rawRepo.id || slug.split("/").pop() || "")
1280
+ .trim()
1281
+ .replace(/\.git$/i, "");
1271
1282
  if (!name) return null;
1272
1283
  const repoPath = resolve(workspacePath, name);
1273
1284
  return {
1274
1285
  name,
1275
1286
  id: normalizeKey(name),
1276
1287
  path: repoPath,
1277
- slug: String(repo.slug || "").trim(),
1278
- url: String(repo.url || "").trim(),
1288
+ slug,
1289
+ url:
1290
+ String(rawRepo.url || "").trim() ||
1291
+ (slug ? `https://github.com/${slug}.git` : ""),
1279
1292
  workspace: String(targetWorkspace.id || "").trim(),
1280
1293
  primary:
1281
- repo.primary === true ||
1294
+ rawRepo.primary === true ||
1282
1295
  (activeRepoName && normalizeKey(name) === activeRepoName) ||
1283
1296
  (!activeRepoName && index === 0),
1284
1297
  };
@@ -1608,14 +1621,22 @@ export function loadConfig(argv = process.argv, options = {}) {
1608
1621
  ? codexEnabled
1609
1622
  : primaryAgent === "copilot-sdk"
1610
1623
  ? !isEnvEnabled(process.env.COPILOT_SDK_DISABLED, false)
1611
- : !isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false);
1624
+ : primaryAgent === "claude-sdk"
1625
+ ? !isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false)
1626
+ : primaryAgent === "gemini-sdk"
1627
+ ? !isEnvEnabled(process.env.GEMINI_SDK_DISABLED, false)
1628
+ : primaryAgent === "opencode-sdk"
1629
+ ? !isEnvEnabled(process.env.OPENCODE_SDK_DISABLED, false)
1630
+ : false;
1612
1631
 
1613
1632
  // agentPoolEnabled: true when ANY agent SDK is available for pooled operations
1614
1633
  // This decouples pooled prompt execution from specific SDK selection
1615
1634
  const agentPoolEnabled =
1616
1635
  !isEnvEnabled(process.env.CODEX_SDK_DISABLED, false) ||
1617
1636
  !isEnvEnabled(process.env.COPILOT_SDK_DISABLED, false) ||
1618
- !isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false);
1637
+ !isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false) ||
1638
+ !isEnvEnabled(process.env.GEMINI_SDK_DISABLED, false) ||
1639
+ !isEnvEnabled(process.env.OPENCODE_SDK_DISABLED, false);
1619
1640
 
1620
1641
  // ── Internal Executor ────────────────────────────────────
1621
1642
  // Allows the monitor to run tasks via agent-pool directly instead of
@@ -1876,6 +1897,38 @@ export function loadConfig(argv = process.argv, options = {}) {
1876
1897
  internalExecutorConfig.backlogReplenishment?.requirePriority !== false,
1877
1898
  ),
1878
1899
  },
1900
+ stream: {
1901
+ maxRetries: Number(
1902
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_RETRIES ||
1903
+ internalExecutorConfig.stream?.maxRetries ||
1904
+ 5,
1905
+ ),
1906
+ retryBaseMs: Number(
1907
+ process.env.INTERNAL_EXECUTOR_STREAM_RETRY_BASE_MS ||
1908
+ internalExecutorConfig.stream?.retryBaseMs ||
1909
+ 2000,
1910
+ ),
1911
+ retryMaxMs: Number(
1912
+ process.env.INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS ||
1913
+ internalExecutorConfig.stream?.retryMaxMs ||
1914
+ 32000,
1915
+ ),
1916
+ firstEventTimeoutMs: Number(
1917
+ process.env.INTERNAL_EXECUTOR_STREAM_FIRST_EVENT_TIMEOUT_MS ||
1918
+ internalExecutorConfig.stream?.firstEventTimeoutMs ||
1919
+ 120000,
1920
+ ),
1921
+ maxItemsPerTurn: Number(
1922
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEMS_PER_TURN ||
1923
+ internalExecutorConfig.stream?.maxItemsPerTurn ||
1924
+ 600,
1925
+ ),
1926
+ maxItemChars: Number(
1927
+ process.env.INTERNAL_EXECUTOR_STREAM_MAX_ITEM_CHARS ||
1928
+ internalExecutorConfig.stream?.maxItemChars ||
1929
+ 12000,
1930
+ ),
1931
+ },
1879
1932
  projectRequirements,
1880
1933
  };
1881
1934