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
@@ -12,12 +12,60 @@
12
12
  * • MAX_STREAM_RETRIES — shared retry ceiling
13
13
  */
14
14
 
15
+ import { loadConfig } from "./config.mjs";
16
+
17
+ function readInternalExecutorStreamConfig() {
18
+ try {
19
+ const cfg = loadConfig();
20
+ const streamCfg = cfg?.internalExecutor?.stream;
21
+ return streamCfg && typeof streamCfg === "object" ? streamCfg : {};
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function parseNumericSetting({
28
+ envKey,
29
+ configValue,
30
+ fallback,
31
+ min,
32
+ max,
33
+ integer = true,
34
+ }) {
35
+ const raw = process.env[envKey];
36
+ const candidate =
37
+ raw !== undefined && String(raw).trim() !== "" ? Number(raw) : Number(configValue);
38
+ const normalized = Number.isFinite(candidate) ? candidate : fallback;
39
+ const bounded = Math.min(Math.max(normalized, min), max);
40
+ return integer ? Math.trunc(bounded) : bounded;
41
+ }
42
+
43
+ const streamConfig = readInternalExecutorStreamConfig();
44
+
15
45
  /** Maximum number of stream-level retry attempts (not counting the first attempt). */
16
- export const MAX_STREAM_RETRIES = 5;
46
+ export const MAX_STREAM_RETRIES = parseNumericSetting({
47
+ envKey: "INTERNAL_EXECUTOR_STREAM_MAX_RETRIES",
48
+ configValue: streamConfig.maxRetries,
49
+ fallback: 5,
50
+ min: 1,
51
+ max: 12,
52
+ });
17
53
 
18
54
  /** Base backoff in ms. Doubles per attempt: 2 s → 4 s → 8 s → 16 s → 32 s. */
19
- const STREAM_RETRY_BASE_MS = 2_000;
20
- const STREAM_RETRY_MAX_MS = 32_000;
55
+ const STREAM_RETRY_BASE_MS = parseNumericSetting({
56
+ envKey: "INTERNAL_EXECUTOR_STREAM_RETRY_BASE_MS",
57
+ configValue: streamConfig.retryBaseMs,
58
+ fallback: 2_000,
59
+ min: 250,
60
+ max: 120_000,
61
+ });
62
+ const STREAM_RETRY_MAX_MS = parseNumericSetting({
63
+ envKey: "INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS",
64
+ configValue: streamConfig.retryMaxMs,
65
+ fallback: 32_000,
66
+ min: STREAM_RETRY_BASE_MS,
67
+ max: 300_000,
68
+ });
21
69
 
22
70
  /**
23
71
  * Returns true for transient stream / network errors that are safe to retry
@@ -29,7 +77,13 @@ const STREAM_RETRY_MAX_MS = 32_000;
29
77
  * @returns {boolean}
30
78
  */
31
79
  export function isTransientStreamError(err) {
32
- const msg = (err?.message || String(err || "")).toLowerCase();
80
+ const rawMessage =
81
+ typeof err?.message === "string"
82
+ ? err.message
83
+ : typeof err === "string"
84
+ ? err
85
+ : "";
86
+ const msg = rawMessage.toLowerCase();
33
87
  return (
34
88
  // ── Codex / Realtime API ────────────────────────────────────────────────
35
89
  msg.includes("stream disconnected") ||
package/sync-engine.mjs CHANGED
@@ -811,7 +811,7 @@ export class SyncEngine {
811
811
  );
812
812
  void this
813
813
  .#notifyTelegram(
814
- `⚠️ Sync engine: ${this.#consecutiveFailures} consecutive failures, backing off to 5 min interval`,
814
+ `:alert: Sync engine: ${this.#consecutiveFailures} consecutive failures, backing off to 5 min interval`,
815
815
  )
816
816
  .catch(() => {});
817
817
  }
@@ -1121,7 +1121,7 @@ export class SyncEngine {
1121
1121
  context,
1122
1122
  timestamp: new Date().toISOString(),
1123
1123
  };
1124
- void this.#notifyTelegram(`⚠️ ${message}`).catch(() => {});
1124
+ void this.#notifyTelegram(`:alert: ${message}`).catch(() => {});
1125
1125
  if (typeof this.#onAlert === "function") {
1126
1126
  try {
1127
1127
  this.#onAlert(payload);
@@ -371,15 +371,15 @@ export async function assessTask(ctx, opts) {
371
371
  if (opts.onTelegram) {
372
372
  const emoji =
373
373
  {
374
- merge: "",
375
- reprompt_same: "💬",
376
- reprompt_new_session: "🔄",
377
- new_attempt: "🆕",
378
- wait: "",
379
- manual_review: "👀",
380
- close_and_replan: "🚫",
381
- noop: "",
382
- }[decision.action] || "";
374
+ merge: ":check:",
375
+ reprompt_same: ":chat:",
376
+ reprompt_new_session: ":refresh:",
377
+ new_attempt: ":star:",
378
+ wait: ":clock:",
379
+ manual_review: ":eye:",
380
+ close_and_replan: ":ban:",
381
+ noop: ":dot:",
382
+ }[decision.action] || ":help:";
383
383
  opts.onTelegram(
384
384
  `${emoji} Assessment [${ctx.shortId}] ${ctx.trigger}: **${decision.action}**\n${decision.reason || ""}`.slice(
385
385
  0,
package/task-cli.mjs CHANGED
@@ -658,7 +658,7 @@ async function cliPlan(args) {
658
658
  console.log(` ${t.id?.slice(0, 8)} ${t.title}`);
659
659
  }
660
660
  } else {
661
- console.log(" Planner ran but no new tasks were created.");
661
+ console.log(" :alert: Planner ran but no new tasks were created.");
662
662
  }
663
663
  console.log("");
664
664
  } catch (err) {
@@ -2,7 +2,7 @@
2
2
  * task-complexity.mjs — Task complexity routing for bosun.
3
3
  *
4
4
  * Maps task size/complexity to appropriate AI models and reasoning effort
5
- * levels. Each executor type (CODEX, COPILOT/Claude) has its own model tier
5
+ * levels. Each executor type has its own model tier
6
6
  * ladder, so small tasks use cheaper/faster models while complex tasks get
7
7
  * the most capable models.
8
8
  *
@@ -89,6 +89,40 @@ export const DEFAULT_MODEL_PROFILES = Object.freeze({
89
89
  reasoningEffort: "high",
90
90
  },
91
91
  },
92
+ GEMINI: {
93
+ [COMPLEXITY_TIERS.LOW]: {
94
+ model: "gemini-2.5-flash",
95
+ variant: "GEMINI_2_5_FLASH",
96
+ reasoningEffort: "low",
97
+ },
98
+ [COMPLEXITY_TIERS.MEDIUM]: {
99
+ model: "gemini-2.5-pro",
100
+ variant: "DEFAULT",
101
+ reasoningEffort: "medium",
102
+ },
103
+ [COMPLEXITY_TIERS.HIGH]: {
104
+ model: "gemini-2.5-pro",
105
+ variant: "GEMINI_2_5_PRO",
106
+ reasoningEffort: "high",
107
+ },
108
+ },
109
+ OPENCODE: {
110
+ [COMPLEXITY_TIERS.LOW]: {
111
+ model: "gpt-5.1-codex-mini",
112
+ variant: "DEFAULT",
113
+ reasoningEffort: "low",
114
+ },
115
+ [COMPLEXITY_TIERS.MEDIUM]: {
116
+ model: "gpt-5.2-codex",
117
+ variant: "DEFAULT",
118
+ reasoningEffort: "medium",
119
+ },
120
+ [COMPLEXITY_TIERS.HIGH]: {
121
+ model: "gpt-5.3-codex",
122
+ variant: "DEFAULT",
123
+ reasoningEffort: "high",
124
+ },
125
+ },
92
126
  });
93
127
 
94
128
  /**
@@ -107,6 +141,12 @@ export const MODEL_ALIASES = Object.freeze({
107
141
  "sonnet-4.5": { executor: "COPILOT", variant: "CLAUDE_SONNET_4_5" },
108
142
  "haiku-4.5": { executor: "COPILOT", variant: "CLAUDE_HAIKU_4_5" },
109
143
  "claude-code": { executor: "COPILOT", variant: "CLAUDE_CODE" },
144
+ "gemini-2.5-flash": { executor: "GEMINI", variant: "GEMINI_2_5_FLASH" },
145
+ "gemini-2.5-pro": { executor: "GEMINI", variant: "DEFAULT" },
146
+ "gemini-2.0-flash": { executor: "GEMINI", variant: "GEMINI_2_0_FLASH" },
147
+ "gemini-1.5-pro": { executor: "GEMINI", variant: "GEMINI_1_5_PRO" },
148
+ "gemini-1.5-flash": { executor: "GEMINI", variant: "GEMINI_1_5_FLASH" },
149
+ "opencode-default": { executor: "OPENCODE", variant: "DEFAULT" },
110
150
  });
111
151
 
112
152
  export const EXECUTOR_MODEL_REGISTRY = Object.freeze({
@@ -134,6 +174,23 @@ export const EXECUTOR_MODEL_REGISTRY = Object.freeze({
134
174
  "claude-haiku-4.5",
135
175
  "claude-code",
136
176
  ]),
177
+ gemini: Object.freeze([
178
+ "gemini-2.5-pro",
179
+ "gemini-2.5-flash",
180
+ "gemini-2.0-flash",
181
+ "gemini-1.5-pro",
182
+ "gemini-1.5-flash",
183
+ ]),
184
+ opencode: Object.freeze([
185
+ "gpt-5.3-codex",
186
+ "gpt-5.2-codex",
187
+ "gpt-5.1-codex",
188
+ "gpt-5.1-codex-mini",
189
+ "claude-opus-4.6",
190
+ "claude-sonnet-4.6",
191
+ "gemini-2.5-pro",
192
+ "gemini-2.5-flash",
193
+ ]),
137
194
  });
138
195
 
139
196
  const EXECUTOR_KEY_ALIASES = Object.freeze({
@@ -148,9 +205,19 @@ const EXECUTOR_KEY_ALIASES = Object.freeze({
148
205
  "claude-code": "claude",
149
206
  "claudecode-sdk": "claude",
150
207
  "claudecode-cli": "claude",
208
+ gemini: "gemini",
209
+ "gemini-sdk": "gemini",
210
+ "gemini-cli": "gemini",
211
+ "google-gemini": "gemini",
212
+ opencode: "opencode",
213
+ "opencode-sdk": "opencode",
214
+ "opencode-cli": "opencode",
215
+ "open-code": "opencode",
151
216
  "CODEX": "codex",
152
217
  "COPILOT": "copilot",
153
218
  "CLAUDE": "claude",
219
+ "GEMINI": "gemini",
220
+ "OPENCODE": "opencode",
154
221
  });
155
222
 
156
223
  export function normalizeExecutorKey(executor) {
@@ -437,7 +504,7 @@ export function formatComplexityDecision(resolved) {
437
504
  */
438
505
  export function getComplexityMatrix(configOverrides) {
439
506
  const matrix = {};
440
- for (const executorType of ["CODEX", "COPILOT"]) {
507
+ for (const executorType of Object.keys(DEFAULT_MODEL_PROFILES)) {
441
508
  matrix[executorType] = {};
442
509
  for (const tier of Object.values(COMPLEXITY_TIERS)) {
443
510
  matrix[executorType][tier] = getModelForComplexity(
@@ -658,5 +725,7 @@ export function executorToSdk(executorType) {
658
725
  const normalized = (executorType || "").toUpperCase();
659
726
  if (normalized === "CLAUDE") return "claude";
660
727
  if (normalized === "COPILOT") return "copilot";
728
+ if (normalized === "GEMINI") return "gemini";
729
+ if (normalized === "OPENCODE") return "opencode";
661
730
  return "codex";
662
731
  }
package/task-context.mjs CHANGED
@@ -178,7 +178,7 @@ export function shouldAutoInstallGitHooks(options = {}) {
178
178
  options.mode ??
179
179
  env.BOSUN_AUTO_GIT_HOOKS_MODE ??
180
180
  env.BOSUN_GIT_HOOKS_MODE,
181
- MODE_TASK,
181
+ MODE_ALWAYS,
182
182
  );
183
183
  if (mode === MODE_OFF) return false;
184
184
  if (mode === MODE_ALWAYS) return true;
@@ -197,4 +197,3 @@ export function shouldRunAgentHookBridge(env = process.env) {
197
197
  if (isEnvFlagEnabled(env.BOSUN_HOOKS_FORCE, false)) return true;
198
198
  return hasBosunTaskContext(env);
199
199
  }
200
-
package/task-executor.mjs CHANGED
@@ -447,7 +447,11 @@ function normalizeSdkOverride(value) {
447
447
  if (raw === "codex-sdk") return "codex";
448
448
  if (raw === "copilot-sdk") return "copilot";
449
449
  if (raw === "claude-sdk") return "claude";
450
- if (["codex", "copilot", "claude"].includes(raw)) return raw;
450
+ if (raw === "gemini-sdk") return "gemini";
451
+ if (raw === "opencode-sdk") return "opencode";
452
+ if (["codex", "copilot", "claude", "gemini", "opencode"].includes(raw)) {
453
+ return raw;
454
+ }
451
455
  return null;
452
456
  }
453
457
 
@@ -2804,7 +2808,7 @@ class TaskExecutor {
2804
2808
  if (progress.idleMs >= STREAM_STALLED_KILL_MS) {
2805
2809
  const elapsedMin = Math.round(elapsed / 60000);
2806
2810
  console.warn(
2807
- `${TAG} ⚠️ WATCHDOG: agent "${slot.taskTitle}" stalled for ` +
2811
+ `${TAG} :alert: WATCHDOG: agent "${slot.taskTitle}" stalled for ` +
2808
2812
  `${Math.round(progress.idleMs / 60000)}min after ${continueCount} continues ` +
2809
2813
  `(total runtime: ${elapsedMin}min, events: ${progress.totalEvents}) — force-aborting`,
2810
2814
  );
@@ -2813,7 +2817,7 @@ class TaskExecutor {
2813
2817
  }
2814
2818
  this._taskCooldowns.set(taskId, now);
2815
2819
  this.sendTelegram?.(
2816
- `⚠️ Watchdog killed stalled agent: "${slot.taskTitle}" ` +
2820
+ `:alert: Watchdog killed stalled agent: "${slot.taskTitle}" ` +
2817
2821
  `(idle ${Math.round(progress.idleMs / 60000)}min after ${continueCount} continues, ` +
2818
2822
  `total ${elapsedMin}min, ${progress.totalEvents} events)`,
2819
2823
  );
@@ -2826,7 +2830,7 @@ class TaskExecutor {
2826
2830
  const elapsedMin = Math.round(elapsed / 60000);
2827
2831
  const deadlineMin = Math.round(absoluteDeadline / 60000);
2828
2832
  console.warn(
2829
- `${TAG} ⚠️ WATCHDOG: absolute deadline exceeded for "${slot.taskTitle}" ` +
2833
+ `${TAG} :alert: WATCHDOG: absolute deadline exceeded for "${slot.taskTitle}" ` +
2830
2834
  `(${elapsedMin}min > ${deadlineMin}min, events: ${progress.totalEvents}, ` +
2831
2835
  `edits: ${progress.hasEdits}, commits: ${progress.hasCommits}) — force-aborting`,
2832
2836
  );
@@ -2839,7 +2843,7 @@ class TaskExecutor {
2839
2843
  }
2840
2844
  this._taskCooldowns.set(taskId, now);
2841
2845
  this.sendTelegram?.(
2842
- `⚠️ Watchdog hard limit: "${slot.taskTitle}" (${elapsedMin}min > ${deadlineMin}min absolute limit)`,
2846
+ `:alert: Watchdog hard limit: "${slot.taskTitle}" (${elapsedMin}min > ${deadlineMin}min absolute limit)`,
2843
2847
  );
2844
2848
  }
2845
2849
  }
@@ -3918,6 +3922,10 @@ class TaskExecutor {
3918
3922
  task?.meta?.repo ||
3919
3923
  "",
3920
3924
  );
3925
+ // Multi-repo: task.repositories is an array of repo slugs/ids
3926
+ const taskRepositories = Array.isArray(task?.repositories) && task.repositories.length > 0
3927
+ ? task.repositories.map((r) => normalizeSelector(r)).filter(Boolean)
3928
+ : taskRepository ? [taskRepository] : [];
3921
3929
 
3922
3930
  const repoCandidates = Array.isArray(this.repositories)
3923
3931
  ? this.repositories
@@ -3928,19 +3936,33 @@ class TaskExecutor {
3928
3936
  )
3929
3937
  : repoCandidates;
3930
3938
 
3931
- const selectedRepo =
3932
- scopedCandidates.find((repo) => normalizeSelector(repo.id) === taskRepository) ||
3933
- scopedCandidates.find((repo) => normalizeSelector(repo.name) === taskRepository) ||
3934
- scopedCandidates.find((repo) => normalizeSelector(repo.slug) === taskRepository) ||
3935
- scopedCandidates.find((repo) => Array.isArray(repo.aliases) && repo.aliases.includes(taskRepository)) ||
3939
+ const resolveOne = (slug) =>
3940
+ scopedCandidates.find((r) => normalizeSelector(r.id) === slug) ||
3941
+ scopedCandidates.find((r) => normalizeSelector(r.name) === slug) ||
3942
+ scopedCandidates.find((r) => normalizeSelector(r.slug) === slug) ||
3943
+ scopedCandidates.find((r) => Array.isArray(r.aliases) && r.aliases.includes(slug)) ||
3936
3944
  null;
3937
3945
 
3938
- const fallbackRepo =
3939
- scopedCandidates.find((repo) => repo.primary) || scopedCandidates[0] || null;
3940
- const effectiveRepo = selectedRepo || fallbackRepo;
3946
+ let resolvedRepos = taskRepositories.map(resolveOne).filter(Boolean);
3947
+
3948
+ // Fallback: if no explicit repos, use primary/first in workspace
3949
+ if (resolvedRepos.length === 0) {
3950
+ const fallbackRepo =
3951
+ scopedCandidates.find((repo) => repo.primary) || scopedCandidates[0] || null;
3952
+ if (fallbackRepo) resolvedRepos = [fallbackRepo];
3953
+ }
3954
+
3955
+ const effectiveRepo = resolvedRepos[0] || null;
3956
+ const isMultiRepo = resolvedRepos.length > 1;
3957
+
3958
+ // For multi-repo: spawn in workspace root (parent of repos) — not a git dir
3959
+ // For single repo: spawn inside that repo directory (git commands work)
3960
+ const repoRoot = isMultiRepo
3961
+ ? (effectiveRepo?.path ? dirname(effectiveRepo.path) : this.repoRoot)
3962
+ : (effectiveRepo?.path || this.repoRoot);
3941
3963
 
3942
3964
  return {
3943
- repoRoot: effectiveRepo?.path || this.repoRoot,
3965
+ repoRoot,
3944
3966
  repoSlug: effectiveRepo?.slug || this.repoSlug || "",
3945
3967
  workspace:
3946
3968
  taskWorkspace ||
@@ -3950,6 +3972,8 @@ class TaskExecutor {
3950
3972
  taskRepository ||
3951
3973
  normalizeSelector(effectiveRepo?.id || effectiveRepo?.name || "") ||
3952
3974
  "",
3975
+ resolvedRepos,
3976
+ isMultiRepo,
3953
3977
  };
3954
3978
  }
3955
3979
 
@@ -4010,6 +4034,9 @@ class TaskExecutor {
4010
4034
  const taskRepoContext = this._resolveTaskRepoContext(task);
4011
4035
  const executionRepoRoot = taskRepoContext.repoRoot || this.repoRoot;
4012
4036
  const executionRepoSlug = taskRepoContext.repoSlug || this.repoSlug;
4037
+ // Workspaces are plain folders; repos live inside them. Only run git/worktree
4038
+ // operations when executionRepoRoot is actually a git repository.
4039
+ const executionRepoIsGit = existsSync(resolve(executionRepoRoot, ".git"));
4013
4040
  const worktreeManager = this._getWorktreeManager(executionRepoRoot);
4014
4041
  let taskClaimToken = null;
4015
4042
  let attemptId = null;
@@ -4235,7 +4262,7 @@ class TaskExecutor {
4235
4262
  commentOnIssue(
4236
4263
  task,
4237
4264
  [
4238
- `## ⏭️ Task Deferred`,
4265
+ `## :play: Task Deferred`,
4239
4266
  ``,
4240
4267
  `This task is currently claimed by another orchestrator instance.`,
4241
4268
  ``,
@@ -4298,7 +4325,7 @@ class TaskExecutor {
4298
4325
  commentOnIssue(
4299
4326
  task,
4300
4327
  [
4301
- `## 🤖 Agent Started`,
4328
+ `## :bot: Agent Started`,
4302
4329
  ``,
4303
4330
  `| Field | Value |`,
4304
4331
  `|-------|-------|`,
@@ -4320,8 +4347,14 @@ class TaskExecutor {
4320
4347
  /* best-effort */
4321
4348
  });
4322
4349
 
4323
- // 3. Acquire worktree
4350
+ // 3. Acquire worktree (only when executionRepoRoot is a git repository;
4351
+ // plain workspace folders with repos inside skip worktree creation and
4352
+ // spawn the agent directly in the resolved directory).
4324
4353
  let wt;
4354
+ if (!executionRepoIsGit) {
4355
+ // Non-git workspace root — agent spawns directly here
4356
+ wt = { path: executionRepoRoot, created: false, noGit: true };
4357
+ } else {
4325
4358
  try {
4326
4359
  const taskBaseBranch = resolveTaskBaseBranch(
4327
4360
  task,
@@ -4393,6 +4426,7 @@ class TaskExecutor {
4393
4426
  this.onTaskFailed?.(task, wrappedError);
4394
4427
  return;
4395
4428
  }
4429
+ } // end executionRepoIsGit
4396
4430
 
4397
4431
  slot.worktreePath = wt.path;
4398
4432
  this._upsertRuntimeSlot(slot);
@@ -4449,6 +4483,8 @@ class TaskExecutor {
4449
4483
  repoSlug: executionRepoSlug,
4450
4484
  workspace: taskRepoContext.workspace,
4451
4485
  repository: taskRepoContext.repository,
4486
+ resolvedRepos: taskRepoContext.resolvedRepos,
4487
+ isMultiRepo: taskRepoContext.isMultiRepo,
4452
4488
  });
4453
4489
 
4454
4490
  // 6b. Create per-task AbortController for watchdog integration
@@ -4475,7 +4511,7 @@ class TaskExecutor {
4475
4511
  "BOSUN_TASK_ID", "BOSUN_TASK_TITLE", "BOSUN_TASK_DESCRIPTION",
4476
4512
  "BOSUN_BRANCH_NAME", "BOSUN_WORKTREE_PATH", "BOSUN_SDK", "BOSUN_MANAGED",
4477
4513
  "BOSUN_REPO_ROOT", "BOSUN_REPO_SLUG", "BOSUN_WORKSPACE", "BOSUN_REPOSITORY",
4478
- "BOSUN_AGENT_REPO_ROOT",
4514
+ "BOSUN_AGENT_REPO_ROOT", "BOSUN_REPOS",
4479
4515
  ];
4480
4516
  const _savedEnv = {};
4481
4517
  for (const k of _savedEnvKeys) _savedEnv[k] = process.env[k];
@@ -4508,6 +4544,16 @@ class TaskExecutor {
4508
4544
  // Enforce workspace isolation: agents must resolve repo root from the
4509
4545
  // workspace-scoped executionRepoRoot, NOT the developer's personal repo.
4510
4546
  process.env.BOSUN_AGENT_REPO_ROOT = executionRepoRoot;
4547
+ // All resolved repos for this task (JSON array of {slug,name,path} objects)
4548
+ process.env.BOSUN_REPOS = taskRepoContext.resolvedRepos?.length
4549
+ ? JSON.stringify(
4550
+ taskRepoContext.resolvedRepos.map((r) => ({
4551
+ slug: r.slug || r.id || "",
4552
+ name: r.name || "",
4553
+ path: r.path || "",
4554
+ })),
4555
+ )
4556
+ : "";
4511
4557
 
4512
4558
  attemptId = `${taskId}-${randomUUID()}`;
4513
4559
  const taskMeta = {
@@ -4779,10 +4825,12 @@ class TaskExecutor {
4779
4825
 
4780
4826
  // 8. Cleanup
4781
4827
  this._slotAbortControllers.delete(taskId);
4782
- try {
4783
- await worktreeManager.releaseWorktree(taskId);
4784
- } catch (err) {
4785
- console.warn(`${TAG} worktree release warning: ${err.message}`);
4828
+ if (executionRepoIsGit) {
4829
+ try {
4830
+ await worktreeManager.releaseWorktree(taskId);
4831
+ } catch (err) {
4832
+ console.warn(`${TAG} worktree release warning: ${err.message}`);
4833
+ }
4786
4834
  }
4787
4835
  await releaseTaskClaimLock();
4788
4836
  this._activeSlots.delete(taskId);
@@ -4806,10 +4854,12 @@ class TaskExecutor {
4806
4854
  } catch {
4807
4855
  /* best-effort */
4808
4856
  }
4809
- try {
4810
- await worktreeManager.releaseWorktree(taskId);
4811
- } catch {
4812
- /* best-effort */
4857
+ if (executionRepoIsGit) {
4858
+ try {
4859
+ await worktreeManager.releaseWorktree(taskId);
4860
+ } catch {
4861
+ /* best-effort */
4862
+ }
4813
4863
  }
4814
4864
  await releaseTaskClaimLock();
4815
4865
 
@@ -4837,7 +4887,7 @@ class TaskExecutor {
4837
4887
  }
4838
4888
  this.onTaskFailed?.(task, err);
4839
4889
  this.sendTelegram?.(
4840
- `❌ Task executor error: "${taskTitle}" — ${(err.message || "").slice(0, 200)}`,
4890
+ `:close: Task executor error: "${taskTitle}" — ${(err.message || "").slice(0, 200)}`,
4841
4891
  );
4842
4892
  }
4843
4893
  }
@@ -4899,6 +4949,19 @@ class TaskExecutor {
4899
4949
  promptRepository ? `- Repository Name: ${promptRepository}` : "",
4900
4950
  `- Repo Root: ${promptRepoRoot}`,
4901
4951
  ``,
4952
+ );
4953
+
4954
+ // For multi-repo tasks: tell the agent exactly where each repo lives
4955
+ if (opts?.isMultiRepo && Array.isArray(opts?.resolvedRepos) && opts.resolvedRepos.length > 1) {
4956
+ lines.push(
4957
+ `## Workspace Repositories`,
4958
+ `This task spans multiple repositories. Resolve all file paths relative to the repo roots below:`,
4959
+ ...opts.resolvedRepos.map((r) => `- **${r.name || r.slug}**: \`${r.path}\``),
4960
+ ``,
4961
+ );
4962
+ }
4963
+
4964
+ lines.push(
4902
4965
  `## Instructions`,
4903
4966
  `You are working autonomously on a software engineering task for this repository.`,
4904
4967
  `Autonomous mode is mandatory for this run — do not pause for approvals, confirmations, or user prompts.`,
@@ -5387,7 +5450,7 @@ class TaskExecutor {
5387
5450
  ? ` Examples: ${verify.sampleTitles.join(", ")}`
5388
5451
  : "";
5389
5452
  this.sendTelegram?.(
5390
- `✅ Planner task completed: "${task.title}" (${verify.createdCount} new task(s)).${sample}`,
5453
+ `:check: Planner task completed: "${task.title}" (${verify.createdCount} new task(s)).${sample}`,
5391
5454
  );
5392
5455
  this.onTaskCompleted?.(task, result);
5393
5456
  return;
@@ -5432,7 +5495,7 @@ class TaskExecutor {
5432
5495
  `${tag} completed with no code changes (expected for preflight/diagnostic task)`,
5433
5496
  );
5434
5497
  this.sendTelegram?.(
5435
- `✅ Task completed with no code changes: "${task.title}"`,
5498
+ `:check: Task completed with no code changes: "${task.title}"`,
5436
5499
  );
5437
5500
  this.onTaskCompleted?.(task, result);
5438
5501
  return;
@@ -5470,7 +5533,7 @@ class TaskExecutor {
5470
5533
  `${TAG} TaskComplete hook blocked PR lifecycle handoff: ${hookResult.reason || "unknown reason"}`,
5471
5534
  );
5472
5535
  this.sendTelegram?.(
5473
- `⚠️ TaskComplete hook blocked PR lifecycle handoff for "${task.title}": ${hookResult.reason || "hook validation failed"}`,
5536
+ `:alert: TaskComplete hook blocked PR lifecycle handoff for "${task.title}": ${hookResult.reason || "hook validation failed"}`,
5474
5537
  );
5475
5538
  }
5476
5539
  } catch (hookErr) {
@@ -5515,7 +5578,7 @@ class TaskExecutor {
5515
5578
  /* best-effort */
5516
5579
  }
5517
5580
  this.sendTelegram?.(
5518
- `✅ Task completed: "${task.title}"\nLifecycle: ${pr.url || pr}`,
5581
+ `:check: Task completed: "${task.title}"\nLifecycle: ${pr.url || pr}`,
5519
5582
  );
5520
5583
 
5521
5584
  // Fire PostPR hook
@@ -5551,7 +5614,7 @@ class TaskExecutor {
5551
5614
  /* best-effort */
5552
5615
  }
5553
5616
  this.sendTelegram?.(
5554
- `✅ Task completed: "${task.title}" (PR lifecycle handoff failed — manual review needed)`,
5617
+ `:check: Task completed: "${task.title}" (PR lifecycle handoff failed — manual review needed)`,
5555
5618
  );
5556
5619
  }
5557
5620
  } else if (hasCommits) {
@@ -5584,7 +5647,7 @@ class TaskExecutor {
5584
5647
  /* best-effort */
5585
5648
  }
5586
5649
  this.sendTelegram?.(
5587
- `✅ Task completed: "${task.title}" (auto-PR disabled)`,
5650
+ `:check: Task completed: "${task.title}" (auto-PR disabled)`,
5588
5651
  );
5589
5652
  } else {
5590
5653
  // No commits — agent completed without making changes.
@@ -5663,11 +5726,11 @@ class TaskExecutor {
5663
5726
  `${tag} task "${task.title}" blocked — ${MAX_NO_COMMIT_ATTEMPTS} consecutive no-commit completions. Skipping until anti-thrash state is cleared.`,
5664
5727
  );
5665
5728
  this.sendTelegram?.(
5666
- `🚫 Task blocked (${MAX_NO_COMMIT_ATTEMPTS}x no-commit): "${task.title}" — will not retry until anti-thrash state is cleared`,
5729
+ `:ban: Task blocked (${MAX_NO_COMMIT_ATTEMPTS}x no-commit): "${task.title}" — will not retry until anti-thrash state is cleared`,
5667
5730
  );
5668
5731
  } else {
5669
5732
  this.sendTelegram?.(
5670
- `⚠️ Task completed but no commits (${noCommitCount}/${MAX_NO_COMMIT_ATTEMPTS}): "${task.title}" — cooldown ${cooldownMin}m`,
5733
+ `:alert: Task completed but no commits (${noCommitCount}/${MAX_NO_COMMIT_ATTEMPTS}): "${task.title}" — cooldown ${cooldownMin}m`,
5671
5734
  );
5672
5735
  }
5673
5736
  }
@@ -5766,7 +5829,7 @@ class TaskExecutor {
5766
5829
  /* best-effort */
5767
5830
  }
5768
5831
  this.sendTelegram?.(
5769
- `❌ Task failed: "${task.title}" — ${(result.error || "").slice(0, 200)}`,
5832
+ `:close: Task failed: "${task.title}" — ${(result.error || "").slice(0, 200)}`,
5770
5833
  );
5771
5834
  } else {
5772
5835
  console.log(
@@ -5876,7 +5939,7 @@ class TaskExecutor {
5876
5939
  }
5877
5940
  if (created.length > 0) {
5878
5941
  this.sendTelegram?.(
5879
- `♻️ Backlog replenished from "${task.title}": created ${created.length} prioritized follow-up task(s).`,
5942
+ `:repeat: Backlog replenished from "${task.title}": created ${created.length} prioritized follow-up task(s).`,
5880
5943
  );
5881
5944
  }
5882
5945
  }
@@ -6294,7 +6357,7 @@ class TaskExecutor {
6294
6357
  },
6295
6358
  );
6296
6359
  if (directResult.status === 0) {
6297
- console.log(`${TAG} directly merged PR #${prNumber}`);
6360
+ console.log(`${TAG} :check: directly merged PR #${prNumber}`);
6298
6361
  // PR merged → close the linked GitHub issue
6299
6362
  if (task) {
6300
6363
  this._closeIssueAfterMerge(task, prNumber).catch(() => {
@@ -6345,7 +6408,7 @@ class TaskExecutor {
6345
6408
  );
6346
6409
 
6347
6410
  const lines = [
6348
- `## 📝 Agent Completed — Commits & PR`,
6411
+ `## :edit: Agent Completed — Commits & PR`,
6349
6412
  ``,
6350
6413
  `**PR:** ${pr.url || `#${pr.prNumber}`}`,
6351
6414
  `**Branch:** \`${pr.branch}\``,
@@ -6394,7 +6457,7 @@ class TaskExecutor {
6394
6457
  await commentOnIssue(
6395
6458
  task,
6396
6459
  [
6397
- `## Issue Resolved`,
6460
+ `## :check: Issue Resolved`,
6398
6461
  ``,
6399
6462
  `PR #${prNumber} has been merged. Closing this issue.`,
6400
6463
  ``,
@@ -6485,7 +6548,7 @@ class TaskExecutor {
6485
6548
  `${TAG} branch safety guard blocked ${branch}: ${reason}`,
6486
6549
  );
6487
6550
  this.sendTelegram?.(
6488
- `🚨 Branch safety guard blocked push/PR lifecycle handoff for ${branch}: ${reason}`,
6551
+ `:alert: Branch safety guard blocked push/PR lifecycle handoff for ${branch}: ${reason}`,
6489
6552
  );
6490
6553
  const err = new Error(
6491
6554
  `Branch safety guard blocked ${branch}: ${reason}`,