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.
- package/.env.example +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
package/stream-resilience.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
1124
|
+
void this.#notifyTelegram(`:alert: ${message}`).catch(() => {});
|
|
1125
1125
|
if (typeof this.#onAlert === "function") {
|
|
1126
1126
|
try {
|
|
1127
1127
|
this.#onAlert(payload);
|
package/task-assessment.mjs
CHANGED
|
@@ -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("
|
|
661
|
+
console.log(" :alert: Planner ran but no new tasks were created.");
|
|
662
662
|
}
|
|
663
663
|
console.log("");
|
|
664
664
|
} catch (err) {
|
package/task-complexity.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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}
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
|
3932
|
-
scopedCandidates.find((
|
|
3933
|
-
scopedCandidates.find((
|
|
3934
|
-
scopedCandidates.find((
|
|
3935
|
-
scopedCandidates.find((
|
|
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
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
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
|
|
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
|
-
`##
|
|
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
|
-
`##
|
|
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
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
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
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
`##
|
|
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
|
-
`##
|
|
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
|
-
|
|
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}`,
|