bosun 0.34.2 → 0.34.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-pool.mjs +30 -10
- package/bosun.schema.json +5 -0
- package/cli.mjs +121 -1
- package/desktop/main.mjs +37 -1
- package/git-commit-helpers.mjs +83 -0
- package/kanban-adapter.mjs +97 -11
- package/monitor.mjs +179 -39
- package/package.json +2 -1
- package/setup-web-server.mjs +32 -7
- package/task-executor.mjs +7 -10
- package/task-store.mjs +43 -10
- package/telegram-bot.mjs +96 -23
- package/ui/modules/settings-schema.js +6 -0
- package/ui/tabs/control.js +2 -1
- package/ui/tabs/settings.js +21 -3
- package/ui-server.mjs +106 -12
- package/workflow-nodes.mjs +109 -2
- package/workspace-manager.mjs +18 -6
package/monitor.mjs
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
resetMergeStrategyDedup,
|
|
99
99
|
} from "./merge-strategy.mjs";
|
|
100
100
|
import { assessTask, quickAssess } from "./task-assessment.mjs";
|
|
101
|
+
import { getBosunCoAuthorTrailer } from "./git-commit-helpers.mjs";
|
|
101
102
|
import {
|
|
102
103
|
normalizeDedupKey,
|
|
103
104
|
stripAnsi,
|
|
@@ -253,6 +254,60 @@ function getAgentAlertsPath() {
|
|
|
253
254
|
return resolve(repoRoot, ".cache", "agent-work-logs", "agent-alerts.jsonl");
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
function getAgentAlertsStatePath() {
|
|
258
|
+
return resolve(
|
|
259
|
+
repoRoot,
|
|
260
|
+
".cache",
|
|
261
|
+
"agent-work-logs",
|
|
262
|
+
"agent-alert-tail-state.json",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function loadAgentAlertsState() {
|
|
267
|
+
const statePath = getAgentAlertsStatePath();
|
|
268
|
+
try {
|
|
269
|
+
if (!existsSync(statePath)) return;
|
|
270
|
+
const raw = readFileSync(statePath, "utf8");
|
|
271
|
+
const parsed = JSON.parse(raw);
|
|
272
|
+
const offset = Number(parsed?.offset || 0);
|
|
273
|
+
if (Number.isFinite(offset) && offset >= 0) {
|
|
274
|
+
agentAlertsOffset = offset;
|
|
275
|
+
}
|
|
276
|
+
const dedupEntries = Array.isArray(parsed?.dedupEntries)
|
|
277
|
+
? parsed.dedupEntries
|
|
278
|
+
: [];
|
|
279
|
+
agentAlertsDedup.clear();
|
|
280
|
+
for (const entry of dedupEntries) {
|
|
281
|
+
if (!entry || typeof entry !== "object") continue;
|
|
282
|
+
const key = String(entry.key || "").trim();
|
|
283
|
+
const ts = Number(entry.ts || 0);
|
|
284
|
+
if (!key || !Number.isFinite(ts) || ts <= 0) continue;
|
|
285
|
+
agentAlertsDedup.set(key, ts);
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.warn(`[monitor] failed loading alert tail state: ${err.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function saveAgentAlertsState() {
|
|
293
|
+
const statePath = getAgentAlertsStatePath();
|
|
294
|
+
try {
|
|
295
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
296
|
+
const dedupEntries = [...agentAlertsDedup.entries()]
|
|
297
|
+
.sort((a, b) => b[1] - a[1])
|
|
298
|
+
.slice(0, 200)
|
|
299
|
+
.map(([key, ts]) => ({ key, ts }));
|
|
300
|
+
const payload = {
|
|
301
|
+
offset: agentAlertsOffset,
|
|
302
|
+
dedupEntries,
|
|
303
|
+
updatedAt: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.warn(`[monitor] failed saving alert tail state: ${err.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
256
311
|
function rememberAlert(key) {
|
|
257
312
|
agentAlertsDedup.set(key, Date.now());
|
|
258
313
|
if (agentAlertsDedup.size > 200) {
|
|
@@ -590,6 +645,9 @@ async function pollAgentAlerts() {
|
|
|
590
645
|
} catch {
|
|
591
646
|
return;
|
|
592
647
|
}
|
|
648
|
+
if (data.length < agentAlertsOffset) {
|
|
649
|
+
agentAlertsOffset = 0;
|
|
650
|
+
}
|
|
593
651
|
if (!data || data.length <= agentAlertsOffset) return;
|
|
594
652
|
const chunk = data.slice(agentAlertsOffset);
|
|
595
653
|
agentAlertsOffset = data.length;
|
|
@@ -614,11 +672,14 @@ async function pollAgentAlerts() {
|
|
|
614
672
|
telegramChatId &&
|
|
615
673
|
process.env.AGENT_ALERTS_NOTIFY === "true"
|
|
616
674
|
) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
675
|
+
runDetached("agent-alerts:telegram", () =>
|
|
676
|
+
sendTelegramMessage(formatAgentAlert(alert), {
|
|
677
|
+
dedupKey: `agent-alert:${alert.type || "alert"}:${alert.attempt_id || "unknown"}`,
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
620
680
|
}
|
|
621
681
|
}
|
|
682
|
+
saveAgentAlertsState();
|
|
622
683
|
}
|
|
623
684
|
|
|
624
685
|
function startAgentWorkAnalyzer() {
|
|
@@ -648,11 +709,12 @@ function stopAgentWorkAnalyzer() {
|
|
|
648
709
|
|
|
649
710
|
function startAgentAlertTailer() {
|
|
650
711
|
if (agentAlertsTimer) return;
|
|
712
|
+
loadAgentAlertsState();
|
|
651
713
|
agentAlertsTimer = setInterval(() => {
|
|
652
|
-
|
|
714
|
+
runDetached("agent-alerts:poll", pollAgentAlerts);
|
|
653
715
|
}, AGENT_ALERT_POLL_MS);
|
|
654
716
|
agentAlertsTimer.unref?.();
|
|
655
|
-
|
|
717
|
+
runDetached("agent-alerts:poll", pollAgentAlerts);
|
|
656
718
|
}
|
|
657
719
|
|
|
658
720
|
function stopAgentAlertTailer() {
|
|
@@ -2085,7 +2147,9 @@ async function handleMonitorFailure(reason, err) {
|
|
|
2085
2147
|
// Ensure we retry after safe-mode window if still running.
|
|
2086
2148
|
if (!shuttingDown) {
|
|
2087
2149
|
setTimeout(() => {
|
|
2088
|
-
if (!shuttingDown)
|
|
2150
|
+
if (!shuttingDown) {
|
|
2151
|
+
runDetached("start-process:hard-cap-retry", startProcess);
|
|
2152
|
+
}
|
|
2089
2153
|
}, pauseMs + 1000);
|
|
2090
2154
|
}
|
|
2091
2155
|
return;
|
|
@@ -2192,6 +2256,21 @@ function runGuarded(reason, fn) {
|
|
|
2192
2256
|
reportGuardedFailure(reason, err);
|
|
2193
2257
|
}
|
|
2194
2258
|
}
|
|
2259
|
+
function runDetached(label, promiseOrFn) {
|
|
2260
|
+
const logFailure = (err) => {
|
|
2261
|
+
const message = formatMonitorError(err);
|
|
2262
|
+
console.warn(`[monitor] detached ${label} failed: ${message}`);
|
|
2263
|
+
};
|
|
2264
|
+
try {
|
|
2265
|
+
const result =
|
|
2266
|
+
typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
|
|
2267
|
+
if (result && typeof result.then === "function") {
|
|
2268
|
+
result.catch((err) => logFailure(err));
|
|
2269
|
+
}
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
logFailure(err);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2195
2274
|
|
|
2196
2275
|
function safeSetInterval(reason, fn, ms) {
|
|
2197
2276
|
return setInterval(() => runGuarded(`interval:${reason}`, fn), ms);
|
|
@@ -2554,15 +2633,19 @@ function notifyVkError(line) {
|
|
|
2554
2633
|
]
|
|
2555
2634
|
.filter(Boolean)
|
|
2556
2635
|
.join("\n");
|
|
2557
|
-
|
|
2558
|
-
|
|
2636
|
+
runDetached("vk-error:telegram", () =>
|
|
2637
|
+
sendTelegramMessage(message, { parseMode: "HTML" }),
|
|
2638
|
+
);
|
|
2639
|
+
runDetached("vk-recovery:notify", () => triggerVibeKanbanRecovery(line));
|
|
2559
2640
|
}
|
|
2560
2641
|
|
|
2561
2642
|
function notifyCodexTrigger(context) {
|
|
2562
2643
|
if (!telegramToken || !telegramChatId) {
|
|
2563
2644
|
return;
|
|
2564
2645
|
}
|
|
2565
|
-
|
|
2646
|
+
runDetached("codex-trigger:telegram", () =>
|
|
2647
|
+
sendTelegramMessage(`Codex triggered: ${context}`),
|
|
2648
|
+
);
|
|
2566
2649
|
}
|
|
2567
2650
|
|
|
2568
2651
|
async function runCodexRecovery(reason) {
|
|
@@ -2796,7 +2879,10 @@ function scheduleVibeKanbanRestart() {
|
|
|
2796
2879
|
console.log(
|
|
2797
2880
|
`[monitor] restarting vibe-kanban in ${delay}ms (attempt ${vkRestartCount}/${vkMaxRestarts})`,
|
|
2798
2881
|
);
|
|
2799
|
-
setTimeout(
|
|
2882
|
+
setTimeout(
|
|
2883
|
+
() => runDetached("vk-restart:scheduled", startVibeKanbanProcess),
|
|
2884
|
+
delay,
|
|
2885
|
+
);
|
|
2800
2886
|
}
|
|
2801
2887
|
|
|
2802
2888
|
async function canConnectTcp(host, port, timeoutMs = 1200) {
|
|
@@ -2894,7 +2980,7 @@ function restartVibeKanbanProcess() {
|
|
|
2894
2980
|
/* best effort */
|
|
2895
2981
|
}
|
|
2896
2982
|
} else {
|
|
2897
|
-
|
|
2983
|
+
runDetached("vk-restart:manual", startVibeKanbanProcess);
|
|
2898
2984
|
}
|
|
2899
2985
|
}
|
|
2900
2986
|
|
|
@@ -3109,7 +3195,9 @@ function ensureVkLogStream() {
|
|
|
3109
3195
|
}
|
|
3110
3196
|
|
|
3111
3197
|
// Discover any active sessions immediately and keep polling for new sessions
|
|
3112
|
-
|
|
3198
|
+
runDetached("vk-session-discovery:startup", () =>
|
|
3199
|
+
refreshVkSessionStreams("startup"),
|
|
3200
|
+
);
|
|
3113
3201
|
ensureVkSessionDiscoveryLoop();
|
|
3114
3202
|
}
|
|
3115
3203
|
|
|
@@ -3117,7 +3205,9 @@ function ensureVkSessionDiscoveryLoop() {
|
|
|
3117
3205
|
if (vkSessionDiscoveryTimer) return;
|
|
3118
3206
|
if (!Number.isFinite(vkEnsureIntervalMs) || vkEnsureIntervalMs <= 0) return;
|
|
3119
3207
|
vkSessionDiscoveryTimer = setInterval(() => {
|
|
3120
|
-
|
|
3208
|
+
runDetached("vk-session-discovery:periodic", () =>
|
|
3209
|
+
refreshVkSessionStreams("periodic"),
|
|
3210
|
+
);
|
|
3121
3211
|
}, vkEnsureIntervalMs);
|
|
3122
3212
|
}
|
|
3123
3213
|
|
|
@@ -3299,7 +3389,9 @@ async function triggerVibeKanbanRecovery(reason) {
|
|
|
3299
3389
|
const notice = codexEnabled
|
|
3300
3390
|
? `Codex recovery triggered: vibe-kanban unreachable. Attempting restart. (${link})`
|
|
3301
3391
|
: `Vibe-kanban recovery triggered (Codex disabled). Attempting restart. (${link})`;
|
|
3302
|
-
|
|
3392
|
+
runDetached("vk-recovery-notify", () =>
|
|
3393
|
+
sendTelegramMessage(notice, { parseMode: "HTML" }),
|
|
3394
|
+
);
|
|
3303
3395
|
}
|
|
3304
3396
|
await runCodexRecovery(reason || "vibe-kanban unreachable");
|
|
3305
3397
|
restartVibeKanbanProcess();
|
|
@@ -3409,8 +3501,10 @@ async function fetchVk(path, opts = {}) {
|
|
|
3409
3501
|
if (shouldLogVkWarning("network-error")) {
|
|
3410
3502
|
console.warn(`[monitor] fetchVk ${method} ${path} error: ${msg}`);
|
|
3411
3503
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3504
|
+
runDetached("vk-recovery:network", () =>
|
|
3505
|
+
triggerVibeKanbanRecovery(
|
|
3506
|
+
`fetchVk ${method} ${path} network error: ${msg}`,
|
|
3507
|
+
),
|
|
3414
3508
|
);
|
|
3415
3509
|
noteVkErrorBurst("network-error");
|
|
3416
3510
|
}
|
|
@@ -3428,8 +3522,10 @@ async function fetchVk(path, opts = {}) {
|
|
|
3428
3522
|
`[monitor] fetchVk ${method} ${path} error: invalid response object (res=${!!res}, res.ok=${res?.ok})`,
|
|
3429
3523
|
);
|
|
3430
3524
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3525
|
+
runDetached("vk-recovery:invalid-response", () =>
|
|
3526
|
+
triggerVibeKanbanRecovery(
|
|
3527
|
+
`fetchVk ${method} ${path} invalid response object`,
|
|
3528
|
+
),
|
|
3433
3529
|
);
|
|
3434
3530
|
noteVkErrorBurst("invalid-response");
|
|
3435
3531
|
return null;
|
|
@@ -3443,8 +3539,10 @@ async function fetchVk(path, opts = {}) {
|
|
|
3443
3539
|
);
|
|
3444
3540
|
}
|
|
3445
3541
|
if (res.status >= 500) {
|
|
3446
|
-
|
|
3447
|
-
|
|
3542
|
+
runDetached("vk-recovery:http", () =>
|
|
3543
|
+
triggerVibeKanbanRecovery(
|
|
3544
|
+
`fetchVk ${method} ${path} HTTP ${res.status}`,
|
|
3545
|
+
),
|
|
3448
3546
|
);
|
|
3449
3547
|
noteVkErrorBurst("http-5xx");
|
|
3450
3548
|
}
|
|
@@ -3482,8 +3580,10 @@ async function fetchVk(path, opts = {}) {
|
|
|
3482
3580
|
);
|
|
3483
3581
|
}
|
|
3484
3582
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3583
|
+
runDetached("vk-recovery:non-json", () =>
|
|
3584
|
+
triggerVibeKanbanRecovery(
|
|
3585
|
+
`fetchVk ${method} ${path} non-JSON response`,
|
|
3586
|
+
),
|
|
3487
3587
|
);
|
|
3488
3588
|
noteVkErrorBurst("non-json");
|
|
3489
3589
|
if (now - vkNonJsonNotifiedAt > 10 * 60 * 1000) {
|
|
@@ -6986,6 +7086,19 @@ function extractPrNumberFromUrl(prUrl) {
|
|
|
6986
7086
|
return match ? parsePositivePrNumber(match[1]) : null;
|
|
6987
7087
|
}
|
|
6988
7088
|
|
|
7089
|
+
function buildFlowGateMergeBody(taskTitle, taskId) {
|
|
7090
|
+
const trailer = getBosunCoAuthorTrailer();
|
|
7091
|
+
const safeTitle = String(taskTitle || "Task").trim() || "Task";
|
|
7092
|
+
const safeId = String(taskId || "").trim();
|
|
7093
|
+
const lines = [
|
|
7094
|
+
`Merged by Bosun flow gate for: ${safeTitle}`,
|
|
7095
|
+
safeId ? `Task: ${safeId}` : "",
|
|
7096
|
+
"",
|
|
7097
|
+
trailer,
|
|
7098
|
+
].filter(Boolean);
|
|
7099
|
+
return lines.join("\n");
|
|
7100
|
+
}
|
|
7101
|
+
|
|
6989
7102
|
async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
6990
7103
|
if (!isFlowPrimaryEnabled() || !isFlowReviewGateEnabled()) {
|
|
6991
7104
|
return false;
|
|
@@ -7036,6 +7149,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
7036
7149
|
|
|
7037
7150
|
const autoArgs = ["pr", "merge", String(prNumber)];
|
|
7038
7151
|
if (repoSlug) autoArgs.push("--repo", repoSlug);
|
|
7152
|
+
autoArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
7039
7153
|
autoArgs.push("--auto", "--squash");
|
|
7040
7154
|
|
|
7041
7155
|
const autoResult = spawnSync("gh", autoArgs, {
|
|
@@ -7057,6 +7171,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
|
|
|
7057
7171
|
if (/clean status|not in the correct state/i.test(autoErr)) {
|
|
7058
7172
|
const directArgs = ["pr", "merge", String(prNumber)];
|
|
7059
7173
|
if (repoSlug) directArgs.push("--repo", repoSlug);
|
|
7174
|
+
directArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
|
|
7060
7175
|
directArgs.push("--squash");
|
|
7061
7176
|
const directResult = spawnSync("gh", directArgs, {
|
|
7062
7177
|
cwd: repoRoot,
|
|
@@ -7227,10 +7342,12 @@ async function runMergeStrategyAnalysis(ctx, opts = {}) {
|
|
|
7227
7342
|
// Re-run analysis after the wait period
|
|
7228
7343
|
setTimeout(
|
|
7229
7344
|
() => {
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7345
|
+
runDetached("merge-strategy:wait-recheck", () =>
|
|
7346
|
+
runMergeStrategyAnalysis({
|
|
7347
|
+
...ctx,
|
|
7348
|
+
ciStatus: "re-check",
|
|
7349
|
+
}),
|
|
7350
|
+
);
|
|
7234
7351
|
},
|
|
7235
7352
|
(execResult.waitSeconds || 300) * 1000,
|
|
7236
7353
|
);
|
|
@@ -7531,10 +7648,12 @@ async function actOnAssessment(ctx, decision) {
|
|
|
7531
7648
|
const waitSec = decision.waitSeconds || 300;
|
|
7532
7649
|
console.log(`[${tag}] → wait ${waitSec}s`);
|
|
7533
7650
|
setTimeout(() => {
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7651
|
+
runDetached("task-assessment:recheck", () =>
|
|
7652
|
+
runTaskAssessment({
|
|
7653
|
+
...ctx,
|
|
7654
|
+
trigger: "reassessment",
|
|
7655
|
+
}),
|
|
7656
|
+
);
|
|
7538
7657
|
}, waitSec * 1000);
|
|
7539
7658
|
break;
|
|
7540
7659
|
}
|
|
@@ -9253,11 +9372,15 @@ function startTaskPlannerStatusLoop() {
|
|
|
9253
9372
|
if (!taskPlannerStatus.enabled || plannerMode === "disabled") return;
|
|
9254
9373
|
taskPlannerStatus.timer = setInterval(() => {
|
|
9255
9374
|
if (shuttingDown) return;
|
|
9256
|
-
|
|
9375
|
+
runDetached("task-planner-status:interval", () =>
|
|
9376
|
+
publishTaskPlannerStatus("interval"),
|
|
9377
|
+
);
|
|
9257
9378
|
}, taskPlannerStatus.intervalMs);
|
|
9258
9379
|
setTimeout(() => {
|
|
9259
9380
|
if (shuttingDown) return;
|
|
9260
|
-
|
|
9381
|
+
runDetached("task-planner-status:startup", () =>
|
|
9382
|
+
publishTaskPlannerStatus("startup"),
|
|
9383
|
+
);
|
|
9261
9384
|
}, 25_000);
|
|
9262
9385
|
}
|
|
9263
9386
|
|
|
@@ -9952,6 +10075,7 @@ async function startTelegramNotifier() {
|
|
|
9952
10075
|
await checkStatusMilestones();
|
|
9953
10076
|
};
|
|
9954
10077
|
|
|
10078
|
+
|
|
9955
10079
|
// Suppress "Notifier started" message on rapid restarts (e.g. code-change restarts).
|
|
9956
10080
|
// If the last start was <60s ago, skip the notification — just log locally.
|
|
9957
10081
|
const lastStartPath = resolve(
|
|
@@ -9976,10 +10100,16 @@ async function startTelegramNotifier() {
|
|
|
9976
10100
|
`[monitor] notifier restarted (suppressed telegram notification — rapid restart)`,
|
|
9977
10101
|
);
|
|
9978
10102
|
} else {
|
|
9979
|
-
|
|
10103
|
+
runDetached("telegram-notifier:startup", () =>
|
|
10104
|
+
sendTelegramMessage(`${projectName} Orchestrator Notifier started.`),
|
|
10105
|
+
);
|
|
9980
10106
|
}
|
|
9981
|
-
telegramNotifierTimeout = setTimeout(
|
|
9982
|
-
|
|
10107
|
+
telegramNotifierTimeout = setTimeout(() => {
|
|
10108
|
+
runDetached("telegram-notifier:tick", sendUpdate);
|
|
10109
|
+
}, intervalMs);
|
|
10110
|
+
telegramNotifierInterval = setInterval(() => {
|
|
10111
|
+
runDetached("telegram-notifier:tick", sendUpdate);
|
|
10112
|
+
}, intervalMs);
|
|
9983
10113
|
}
|
|
9984
10114
|
|
|
9985
10115
|
async function checkStatusMilestones() {
|
|
@@ -10467,7 +10597,9 @@ async function triggerTaskPlanner(
|
|
|
10467
10597
|
} else {
|
|
10468
10598
|
throw new Error(`Unknown planner mode: ${effectiveMode}`);
|
|
10469
10599
|
}
|
|
10470
|
-
|
|
10600
|
+
runDetached("task-planner-status:trigger-success", () =>
|
|
10601
|
+
publishTaskPlannerStatus("trigger-success"),
|
|
10602
|
+
);
|
|
10471
10603
|
return result;
|
|
10472
10604
|
} catch (err) {
|
|
10473
10605
|
const message = err && err.message ? err.message : String(err);
|
|
@@ -10481,7 +10613,9 @@ async function triggerTaskPlanner(
|
|
|
10481
10613
|
`Task planner run failed (${effectiveMode}): ${message}`,
|
|
10482
10614
|
);
|
|
10483
10615
|
}
|
|
10484
|
-
|
|
10616
|
+
runDetached("task-planner-status:trigger-failed", () =>
|
|
10617
|
+
publishTaskPlannerStatus("trigger-failed"),
|
|
10618
|
+
);
|
|
10485
10619
|
throw err; // re-throw so callers (e.g. /plan command) know it failed
|
|
10486
10620
|
} finally {
|
|
10487
10621
|
plannerTriggered = false;
|
|
@@ -12672,7 +12806,9 @@ async function startProcess() {
|
|
|
12672
12806
|
if (!shuttingDown) {
|
|
12673
12807
|
const retryMs = Math.max(5_000, restartDelayMs || 0);
|
|
12674
12808
|
safeSetTimeout("startProcess-retry", () => {
|
|
12675
|
-
if (!shuttingDown)
|
|
12809
|
+
if (!shuttingDown) {
|
|
12810
|
+
return startProcess();
|
|
12811
|
+
}
|
|
12676
12812
|
}, retryMs);
|
|
12677
12813
|
}
|
|
12678
12814
|
}
|
|
@@ -13184,7 +13320,9 @@ function scheduleEnvReload(reason) {
|
|
|
13184
13320
|
clearTimeout(envWatcherDebounce);
|
|
13185
13321
|
}
|
|
13186
13322
|
envWatcherDebounce = setTimeout(() => {
|
|
13187
|
-
|
|
13323
|
+
runDetached("config-reload:env-change", () =>
|
|
13324
|
+
reloadConfig(reason || "env-change"),
|
|
13325
|
+
);
|
|
13188
13326
|
}, 400);
|
|
13189
13327
|
}
|
|
13190
13328
|
|
|
@@ -14961,3 +15099,5 @@ export {
|
|
|
14961
15099
|
getContainerStatus,
|
|
14962
15100
|
isContainerEnabled,
|
|
14963
15101
|
};
|
|
15102
|
+
|
|
15103
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.4",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"github-oauth-portal.mjs",
|
|
153
153
|
"github-reconciler.mjs",
|
|
154
154
|
"get-telegram-chat-id.mjs",
|
|
155
|
+
"git-commit-helpers.mjs",
|
|
155
156
|
"git-safety.mjs",
|
|
156
157
|
"kanban-adapter.mjs",
|
|
157
158
|
"lib/logger.mjs",
|
package/setup-web-server.mjs
CHANGED
|
@@ -182,6 +182,17 @@ function getCommandVersion(cmd) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
function isWslInteropRuntime() {
|
|
186
|
+
return Boolean(
|
|
187
|
+
process.env.WSL_DISTRO_NAME
|
|
188
|
+
|| process.env.WSL_INTEROP
|
|
189
|
+
|| (process.platform === "win32"
|
|
190
|
+
&& String(process.env.HOME || "")
|
|
191
|
+
.trim()
|
|
192
|
+
.startsWith("/home/")),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
/**
|
|
186
197
|
* Resolve the Bosun Home directory — the single root where bosun stores all its
|
|
187
198
|
* configs, workspaces, and tooling. Resolution order:
|
|
@@ -190,7 +201,7 @@ function getCommandVersion(cmd) {
|
|
|
190
201
|
* 3. cwd if it already has a bosun.config.json AND is not inside a git repo
|
|
191
202
|
* we don't own (safety: prevents "bosun --setup" from contaminating whatever
|
|
192
203
|
* repo the user happened to be in when they ran the command)
|
|
193
|
-
* 4.
|
|
204
|
+
* 4. platform default config home + /bosun (same as runtime loader)
|
|
194
205
|
*/
|
|
195
206
|
function resolveConfigDir() {
|
|
196
207
|
if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
|
|
@@ -211,8 +222,22 @@ function resolveConfigDir() {
|
|
|
211
222
|
if (isExpectedHome) return cwd;
|
|
212
223
|
}
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
const preferWindowsDirs =
|
|
226
|
+
process.platform === "win32" && !isWslInteropRuntime();
|
|
227
|
+
const baseDir = preferWindowsDirs
|
|
228
|
+
? process.env.APPDATA
|
|
229
|
+
|| process.env.LOCALAPPDATA
|
|
230
|
+
|| process.env.USERPROFILE
|
|
231
|
+
|| process.env.HOME
|
|
232
|
+
|| homedir()
|
|
233
|
+
: process.env.HOME
|
|
234
|
+
|| process.env.XDG_CONFIG_HOME
|
|
235
|
+
|| process.env.USERPROFILE
|
|
236
|
+
|| process.env.APPDATA
|
|
237
|
+
|| process.env.LOCALAPPDATA
|
|
238
|
+
|| homedir();
|
|
239
|
+
|
|
240
|
+
return resolve(baseDir, "bosun");
|
|
216
241
|
}
|
|
217
242
|
|
|
218
243
|
/**
|
|
@@ -664,7 +689,7 @@ function handleApply(body) {
|
|
|
664
689
|
envMap.TELEGRAM_UI_ALLOW_UNSAFE = "false";
|
|
665
690
|
}
|
|
666
691
|
if (env.telegramChatId) envMap.TELEGRAM_CHAT_ID = env.telegramChatId;
|
|
667
|
-
if (env.jiraUrl) envMap.
|
|
692
|
+
if (env.jiraUrl) envMap.JIRA_BASE_URL = env.jiraUrl;
|
|
668
693
|
if (env.jiraProjectKey) envMap.JIRA_PROJECT_KEY = env.jiraProjectKey;
|
|
669
694
|
if (env.jiraApiToken) envMap.JIRA_API_TOKEN = env.jiraApiToken;
|
|
670
695
|
if (env.githubProjectNumber) envMap.GITHUB_PROJECT_NUMBER = String(env.githubProjectNumber);
|
|
@@ -674,11 +699,11 @@ function handleApply(body) {
|
|
|
674
699
|
if (env.executorDistribution) envMap.EXECUTOR_DISTRIBUTION = env.executorDistribution;
|
|
675
700
|
if (env.failoverStrategy) envMap.FAILOVER_STRATEGY = env.failoverStrategy;
|
|
676
701
|
if (env.maxParallel != null) envMap.MAX_PARALLEL = String(env.maxParallel);
|
|
677
|
-
if (env.maxRetries != null) envMap.
|
|
702
|
+
if (env.maxRetries != null) envMap.FAILOVER_MAX_RETRIES = String(env.maxRetries);
|
|
678
703
|
if (env.failoverCooldownMinutes != null)
|
|
679
|
-
|
|
704
|
+
envMap.FAILOVER_COOLDOWN_MIN = String(env.failoverCooldownMinutes);
|
|
680
705
|
if (env.failoverDisableOnConsecutive != null)
|
|
681
|
-
|
|
706
|
+
envMap.FAILOVER_DISABLE_AFTER = String(env.failoverDisableOnConsecutive);
|
|
682
707
|
if (env.primaryAgent) envMap.PRIMARY_AGENT = env.primaryAgent;
|
|
683
708
|
if (env.projectRequirementsProfile) envMap.PROJECT_REQUIREMENTS_PROFILE = env.projectRequirementsProfile;
|
|
684
709
|
if (env.internalReplenishEnabled != null)
|
package/task-executor.mjs
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
getRecentCommits,
|
|
72
72
|
collectDiffStats,
|
|
73
73
|
} from "./diff-stats.mjs";
|
|
74
|
+
import { getBosunCoAuthorTrailer } from "./git-commit-helpers.mjs";
|
|
74
75
|
import { createAnomalyDetector } from "./anomaly-detector.mjs";
|
|
75
76
|
import { normalizeDedupKey, yieldToEventLoop, withRetry } from "./utils.mjs";
|
|
76
77
|
import {
|
|
@@ -323,26 +324,22 @@ function categorizeError(err) {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
/**
|
|
326
|
-
* Returns the Co-authored-by trailer for bosun-ve[bot]
|
|
327
|
-
* if the GitHub App ID is not configured. Used to attribute agent commits to
|
|
328
|
-
* the Bosun GitHub App so the bot shows up as a contributor.
|
|
327
|
+
* Returns the Co-authored-by trailer for bosun-ve[bot].
|
|
329
328
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
329
|
+
* Attribution should not be gated by auth mode (OAuth/App/env/gh-cli all valid
|
|
330
|
+
* execution paths). This uses the canonical helper so commit attribution stays
|
|
331
|
+
* consistent with the rest of Bosun.
|
|
332
332
|
*/
|
|
333
333
|
function getBosunCoAuthorLine() {
|
|
334
|
-
|
|
335
|
-
if (!appId) return "";
|
|
336
|
-
return `Co-authored-by: bosun-ve[bot] <${appId}+bosun-ve[bot]@users.noreply.github.com>`;
|
|
334
|
+
return getBosunCoAuthorTrailer();
|
|
337
335
|
}
|
|
338
336
|
|
|
339
337
|
/**
|
|
340
338
|
* Returns a prompt instruction block telling the agent to append the Bosun
|
|
341
|
-
* co-author trailer to every commit.
|
|
339
|
+
* co-author trailer to every commit.
|
|
342
340
|
*/
|
|
343
341
|
function getBosunCoAuthorInstruction() {
|
|
344
342
|
const line = getBosunCoAuthorLine();
|
|
345
|
-
if (!line) return "";
|
|
346
343
|
return `\n**Attribution (required — do not omit):**
|
|
347
344
|
Every commit message MUST end with a blank line then this exact trailer:
|
|
348
345
|
\`\`\`
|
package/task-store.mjs
CHANGED
|
@@ -34,10 +34,34 @@ function inferRepoRoot(startDir) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function resolveBosunHomeDir() {
|
|
38
|
+
const explicit = String(
|
|
39
|
+
process.env.BOSUN_HOME || process.env.BOSUN_DIR || "",
|
|
40
|
+
).trim();
|
|
41
|
+
if (explicit) return resolve(explicit);
|
|
42
|
+
|
|
43
|
+
const base = String(
|
|
44
|
+
process.env.APPDATA ||
|
|
45
|
+
process.env.LOCALAPPDATA ||
|
|
46
|
+
process.env.USERPROFILE ||
|
|
47
|
+
process.env.HOME ||
|
|
48
|
+
"",
|
|
49
|
+
).trim();
|
|
50
|
+
if (!base) return null;
|
|
51
|
+
if (/[/\\]bosun$/i.test(base)) return resolve(base);
|
|
52
|
+
return resolve(base, "bosun");
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
function resolveDefaultStorePath() {
|
|
38
|
-
const repoRoot =
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
const repoRoot = inferRepoRoot(process.cwd());
|
|
57
|
+
if (repoRoot) {
|
|
58
|
+
return resolve(repoRoot, ".bosun", ".cache", "kanban-state.json");
|
|
59
|
+
}
|
|
60
|
+
const bosunHome = resolveBosunHomeDir();
|
|
61
|
+
if (bosunHome) {
|
|
62
|
+
return resolve(bosunHome, ".cache", "kanban-state.json");
|
|
63
|
+
}
|
|
64
|
+
return resolve(__dirname, ".cache", "kanban-state.json");
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
let storePath = resolveDefaultStorePath();
|
|
@@ -58,13 +82,15 @@ let _didLogInitialLoad = false;
|
|
|
58
82
|
|
|
59
83
|
export function configureTaskStore(options = {}) {
|
|
60
84
|
const baseDir = options.baseDir ? resolve(options.baseDir) : null;
|
|
85
|
+
const repoRoot = inferRepoRoot(process.cwd());
|
|
86
|
+
const homeDir = resolveBosunHomeDir();
|
|
87
|
+
const defaultBase = baseDir || repoRoot || homeDir || resolve(__dirname);
|
|
88
|
+
const needsBosunSubdir = Boolean(baseDir || repoRoot);
|
|
61
89
|
const nextPath = options.storePath
|
|
62
90
|
? resolve(baseDir || process.cwd(), options.storePath)
|
|
63
91
|
: resolve(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
resolve(__dirname, "..", ".."),
|
|
67
|
-
".bosun",
|
|
92
|
+
defaultBase,
|
|
93
|
+
needsBosunSubdir ? ".bosun" : "",
|
|
68
94
|
".cache",
|
|
69
95
|
"kanban-state.json",
|
|
70
96
|
);
|
|
@@ -420,14 +446,21 @@ export function setTaskStatus(taskId, status, source) {
|
|
|
420
446
|
}
|
|
421
447
|
|
|
422
448
|
const prev = task.status;
|
|
449
|
+
const tsNow = now();
|
|
423
450
|
task.status = status;
|
|
424
|
-
task.updatedAt =
|
|
425
|
-
task.lastActivityAt =
|
|
451
|
+
task.updatedAt = tsNow;
|
|
452
|
+
task.lastActivityAt = tsNow;
|
|
453
|
+
|
|
454
|
+
// No-op transition: keep activity fresh without polluting history/logs.
|
|
455
|
+
if (prev === status) {
|
|
456
|
+
saveStore();
|
|
457
|
+
return { ...task };
|
|
458
|
+
}
|
|
426
459
|
|
|
427
460
|
// Append to history (FIFO, max 50)
|
|
428
461
|
task.statusHistory.push({
|
|
429
462
|
status,
|
|
430
|
-
timestamp:
|
|
463
|
+
timestamp: tsNow,
|
|
431
464
|
source: source || "unknown",
|
|
432
465
|
});
|
|
433
466
|
if (task.statusHistory.length > MAX_STATUS_HISTORY) {
|