bosun 0.40.11 → 0.40.13
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/cli.mjs +97 -0
- package/git/git-safety.mjs +17 -0
- package/infra/monitor.mjs +93 -1
- package/package.json +1 -1
- package/ui/demo-defaults.js +2065 -1735
- package/workflow/workflow-nodes.mjs +65 -4
- package/workflow-templates/agents.mjs +63 -15
- package/workflow-templates/github.mjs +19 -4
package/cli.mjs
CHANGED
|
@@ -336,6 +336,19 @@ const DAEMON_MAX_INSTANT_RESTARTS = Math.max(
|
|
|
336
336
|
1,
|
|
337
337
|
Number(process.env.BOSUN_DAEMON_MAX_INSTANT_RESTARTS || 5) || 5,
|
|
338
338
|
);
|
|
339
|
+
const DAEMON_MISCONFIG_GUARD_ENABLED = !["0", "false", "off", "no"].includes(
|
|
340
|
+
String(process.env.BOSUN_DAEMON_MISCONFIG_GUARD || "1")
|
|
341
|
+
.trim()
|
|
342
|
+
.toLowerCase(),
|
|
343
|
+
);
|
|
344
|
+
const DAEMON_MISCONFIG_GUARD_MIN_RESTARTS = Math.max(
|
|
345
|
+
1,
|
|
346
|
+
Number(process.env.BOSUN_DAEMON_MISCONFIG_GUARD_MIN_RESTARTS || 3) || 3,
|
|
347
|
+
);
|
|
348
|
+
const DAEMON_MISCONFIG_LOG_SCAN_LINES = Math.max(
|
|
349
|
+
20,
|
|
350
|
+
Number(process.env.BOSUN_DAEMON_MISCONFIG_LOG_SCAN_LINES || 250) || 250,
|
|
351
|
+
);
|
|
339
352
|
let daemonRestartCount = 0;
|
|
340
353
|
const daemonCrashTracker = createDaemonCrashTracker({
|
|
341
354
|
instantCrashWindowMs: DAEMON_INSTANT_CRASH_WINDOW_MS,
|
|
@@ -2080,6 +2093,77 @@ function getMonitorPidFileCandidates(extraCacheDirs = []) {
|
|
|
2080
2093
|
]);
|
|
2081
2094
|
}
|
|
2082
2095
|
|
|
2096
|
+
function tailLinesFromFile(filePath, maxLines = 200) {
|
|
2097
|
+
try {
|
|
2098
|
+
if (!existsSync(filePath)) return [];
|
|
2099
|
+
const raw = readFileSync(filePath, "utf8");
|
|
2100
|
+
if (!raw) return [];
|
|
2101
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
2102
|
+
if (lines.length <= maxLines) return lines;
|
|
2103
|
+
return lines.slice(-maxLines);
|
|
2104
|
+
} catch {
|
|
2105
|
+
return [];
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function detectDaemonRestartStormSignals(options) {
|
|
2110
|
+
const resolvedOptions = options && typeof options === "object" ? options : {};
|
|
2111
|
+
const logDir = resolvedOptions.logDir || resolve(__dirname, "logs");
|
|
2112
|
+
const maxLines = resolvedOptions.maxLines || DAEMON_MISCONFIG_LOG_SCAN_LINES;
|
|
2113
|
+
const reasons = [];
|
|
2114
|
+
const monitorErrorLines = tailLinesFromFile(
|
|
2115
|
+
resolve(logDir, "monitor-error.log"),
|
|
2116
|
+
maxLines,
|
|
2117
|
+
);
|
|
2118
|
+
const monitorLines = tailLinesFromFile(resolve(logDir, "monitor.log"), maxLines);
|
|
2119
|
+
const combined = [...monitorErrorLines, ...monitorLines].join("\n");
|
|
2120
|
+
if (!combined) {
|
|
2121
|
+
return { hasSignal: false, reasons: [] };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (
|
|
2125
|
+
/missing prerequisites:\s*no API key|codex unavailable:\s*no API key/i.test(
|
|
2126
|
+
combined,
|
|
2127
|
+
)
|
|
2128
|
+
) {
|
|
2129
|
+
reasons.push("missing_api_key");
|
|
2130
|
+
}
|
|
2131
|
+
if (
|
|
2132
|
+
/another bosun instance holds the lock|duplicate start ignored|another bosun is already running/i
|
|
2133
|
+
.test(combined)
|
|
2134
|
+
) {
|
|
2135
|
+
reasons.push("duplicate_runtime");
|
|
2136
|
+
}
|
|
2137
|
+
if (/Shared state heartbeat FATAL.*owner_mismatch/i.test(combined)) {
|
|
2138
|
+
reasons.push("shared_state_owner_mismatch");
|
|
2139
|
+
}
|
|
2140
|
+
if (
|
|
2141
|
+
/There is no tracking information for the current branch|git pull <remote> <branch>/i
|
|
2142
|
+
.test(combined)
|
|
2143
|
+
) {
|
|
2144
|
+
reasons.push("workspace_git_tracking_missing");
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
return {
|
|
2148
|
+
hasSignal: reasons.length > 0,
|
|
2149
|
+
reasons,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function shouldPauseDaemonRestartStorm(options) {
|
|
2154
|
+
const resolvedOptions = options && typeof options === "object" ? options : {};
|
|
2155
|
+
const restartCount = Number(resolvedOptions.restartCount || 0);
|
|
2156
|
+
const logDir = resolvedOptions.logDir;
|
|
2157
|
+
if (!IS_DAEMON_CHILD) return { pause: false, reasons: [] };
|
|
2158
|
+
if (!DAEMON_MISCONFIG_GUARD_ENABLED) return { pause: false, reasons: [] };
|
|
2159
|
+
if (restartCount < DAEMON_MISCONFIG_GUARD_MIN_RESTARTS) {
|
|
2160
|
+
return { pause: false, reasons: [] };
|
|
2161
|
+
}
|
|
2162
|
+
const signals = detectDaemonRestartStormSignals({ logDir });
|
|
2163
|
+
if (!signals.hasSignal) return { pause: false, reasons: [] };
|
|
2164
|
+
return { pause: true, reasons: signals.reasons };
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2083
2167
|
function detectExistingMonitorLockOwner(excludePid = null) {
|
|
2084
2168
|
try {
|
|
2085
2169
|
for (const pidFile of getMonitorPidFileCandidates()) {
|
|
@@ -2213,6 +2297,19 @@ function runMonitor({ restartReason = "" } = {}) {
|
|
|
2213
2297
|
DAEMON_MAX_RESTART_DELAY_MS,
|
|
2214
2298
|
);
|
|
2215
2299
|
const delayMs = isOSKill ? 5000 : backoffDelay;
|
|
2300
|
+
const restartStormGuard = shouldPauseDaemonRestartStorm({
|
|
2301
|
+
restartCount: daemonRestartCount,
|
|
2302
|
+
});
|
|
2303
|
+
if (restartStormGuard.pause) {
|
|
2304
|
+
const reasonLabel = restartStormGuard.reasons.join(", ");
|
|
2305
|
+
console.error(
|
|
2306
|
+
`\n :close: Monitor restart storm paused after ${daemonRestartCount} attempts due to persistent runtime issues (${reasonLabel}).`,
|
|
2307
|
+
);
|
|
2308
|
+
sendCrashNotification(exitCode, signal).finally(() =>
|
|
2309
|
+
process.exit(exitCode),
|
|
2310
|
+
);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2216
2313
|
if (IS_DAEMON_CHILD && crashState.exceeded) {
|
|
2217
2314
|
const durationSec = Math.max(
|
|
2218
2315
|
1,
|
package/git/git-safety.mjs
CHANGED
|
@@ -15,6 +15,8 @@ const STRIPPED_GIT_ENV_KEYS = [
|
|
|
15
15
|
const BLOCKED_TEST_GIT_IDENTITIES = new Set([
|
|
16
16
|
"test@example.com",
|
|
17
17
|
"bosun-tests@example.com",
|
|
18
|
+
"bot@example.com",
|
|
19
|
+
"test@test.com",
|
|
18
20
|
]);
|
|
19
21
|
|
|
20
22
|
const TEST_FIXTURE_SENTINEL_PATHS = new Set([
|
|
@@ -264,3 +266,18 @@ export function evaluateBranchSafetyForPush(worktreePath, opts = {}) {
|
|
|
264
266
|
},
|
|
265
267
|
};
|
|
266
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clear any blocked test git identity from a worktree's local config.
|
|
272
|
+
* Worktrees inherit the parent repo's config, so if a test ever set
|
|
273
|
+
* user.name/email there it will poison all task commits until cleared.
|
|
274
|
+
* Call this after acquiring any worktree.
|
|
275
|
+
*/
|
|
276
|
+
export function clearBlockedWorktreeIdentity(worktreePath) {
|
|
277
|
+
const email = getGitConfig(worktreePath, "user.email").toLowerCase();
|
|
278
|
+
if (!BLOCKED_TEST_GIT_IDENTITIES.has(email)) return false;
|
|
279
|
+
|
|
280
|
+
runGit(["config", "--local", "--unset", "user.email"], worktreePath, 5_000);
|
|
281
|
+
runGit(["config", "--local", "--unset", "user.name"], worktreePath, 5_000);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
package/infra/monitor.mjs
CHANGED
|
@@ -197,7 +197,7 @@ import {
|
|
|
197
197
|
installConsoleInterceptor,
|
|
198
198
|
setErrorLogFile,
|
|
199
199
|
} from "../lib/logger.mjs";
|
|
200
|
-
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
200
|
+
import { fixGitConfigCorruption, listActiveWorktrees } from "../workspace/worktree-manager.mjs";
|
|
201
201
|
// ── Task management subsystem imports ──────────────────────────────────────
|
|
202
202
|
import {
|
|
203
203
|
configureTaskStore,
|
|
@@ -14062,6 +14062,98 @@ safeSetInterval("workflow-review-merge-reconcile", async () => {
|
|
|
14062
14062
|
|
|
14063
14063
|
// Legacy merged PR check removed (workflow-only control).
|
|
14064
14064
|
|
|
14065
|
+
// ── Periodic stale worktree sync: every 5 min ─────────────────────────────
|
|
14066
|
+
// Detects task worktrees that are diverged from their remote (local commits
|
|
14067
|
+
// not pushed, or remote has newer commits from a previous run). For each
|
|
14068
|
+
// diverged worktree it: fetches remote, rebases local onto the remote tracking
|
|
14069
|
+
// ref for that branch, then pushes with --force-with-lease. This keeps the
|
|
14070
|
+
// VS Code source-control view clean and unblocks tasks that got stuck mid-push.
|
|
14071
|
+
async function syncDivergedWorktrees() {
|
|
14072
|
+
const worktrees = listActiveWorktrees(repoRoot);
|
|
14073
|
+
let synced = 0;
|
|
14074
|
+
let failed = 0;
|
|
14075
|
+
|
|
14076
|
+
for (const wt of worktrees) {
|
|
14077
|
+
const { path: wtPath, branch } = wt;
|
|
14078
|
+
if (!wtPath || !branch || !branch.startsWith("task/")) continue;
|
|
14079
|
+
|
|
14080
|
+
try {
|
|
14081
|
+
// Fetch remote to update tracking refs
|
|
14082
|
+
execSync("git fetch origin --no-tags", {
|
|
14083
|
+
cwd: wtPath, timeout: 30_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14084
|
+
});
|
|
14085
|
+
|
|
14086
|
+
// Check ahead/behind vs remote tracking ref
|
|
14087
|
+
const remoteRef = `origin/${branch}`;
|
|
14088
|
+
let remoteExists = false;
|
|
14089
|
+
try {
|
|
14090
|
+
execSync(`git rev-parse --verify ${remoteRef}`, {
|
|
14091
|
+
cwd: wtPath, timeout: 5_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14092
|
+
});
|
|
14093
|
+
remoteExists = true;
|
|
14094
|
+
} catch { /* branch not yet pushed — nothing to sync */ }
|
|
14095
|
+
|
|
14096
|
+
if (!remoteExists) continue;
|
|
14097
|
+
|
|
14098
|
+
const ahead = parseInt(
|
|
14099
|
+
execSync(`git rev-list --count ${remoteRef}..HEAD`, {
|
|
14100
|
+
cwd: wtPath, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14101
|
+
}).trim(), 10);
|
|
14102
|
+
const behind = parseInt(
|
|
14103
|
+
execSync(`git rev-list --count HEAD..${remoteRef}`, {
|
|
14104
|
+
cwd: wtPath, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14105
|
+
}).trim(), 10);
|
|
14106
|
+
|
|
14107
|
+
// Only act on diverged worktrees (behind > 0 AND ahead > 0)
|
|
14108
|
+
if (ahead === 0 || behind === 0) continue;
|
|
14109
|
+
|
|
14110
|
+
console.log(
|
|
14111
|
+
`[monitor:worktree-sync] ${branch} diverged: ${ahead} ahead, ${behind} behind — rebasing and pushing`,
|
|
14112
|
+
);
|
|
14113
|
+
|
|
14114
|
+
// Rebase local onto remote tracking ref to incorporate remote commits
|
|
14115
|
+
let rebased = false;
|
|
14116
|
+
try {
|
|
14117
|
+
execSync(`git rebase ${remoteRef}`, {
|
|
14118
|
+
cwd: wtPath, encoding: "utf8", timeout: 60_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14119
|
+
});
|
|
14120
|
+
rebased = true;
|
|
14121
|
+
} catch (rebaseErr) {
|
|
14122
|
+
try { execSync("git rebase --abort", { cwd: wtPath, timeout: 10_000, stdio: ["ignore", "pipe", "pipe"] }); } catch { /* ok */ }
|
|
14123
|
+
console.warn(
|
|
14124
|
+
`[monitor:worktree-sync] ${branch} rebase conflict — skipping push: ${rebaseErr.message?.slice(0, 200)}`,
|
|
14125
|
+
);
|
|
14126
|
+
failed++;
|
|
14127
|
+
continue;
|
|
14128
|
+
}
|
|
14129
|
+
|
|
14130
|
+
// Push with --force-with-lease (safe: we just fetched fresh remote refs)
|
|
14131
|
+
try {
|
|
14132
|
+
execSync(`git push --force-with-lease --set-upstream origin HEAD`, {
|
|
14133
|
+
cwd: wtPath, encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14134
|
+
});
|
|
14135
|
+
console.log(`[monitor:worktree-sync] ${branch} sync-pushed successfully`);
|
|
14136
|
+
synced++;
|
|
14137
|
+
} catch (pushErr) {
|
|
14138
|
+
console.warn(
|
|
14139
|
+
`[monitor:worktree-sync] ${branch} push failed: ${pushErr.message?.slice(0, 200)}`,
|
|
14140
|
+
);
|
|
14141
|
+
failed++;
|
|
14142
|
+
}
|
|
14143
|
+
} catch (err) {
|
|
14144
|
+
console.warn(`[monitor:worktree-sync] ${branch} error: ${err.message?.slice(0, 200)}`);
|
|
14145
|
+
failed++;
|
|
14146
|
+
}
|
|
14147
|
+
}
|
|
14148
|
+
|
|
14149
|
+
if (synced > 0 || failed > 0) {
|
|
14150
|
+
console.log(`[monitor:worktree-sync] cycle complete — synced=${synced} failed=${failed}`);
|
|
14151
|
+
}
|
|
14152
|
+
}
|
|
14153
|
+
|
|
14154
|
+
const worktreeSyncIntervalMs = 5 * 60 * 1000; // 5 min
|
|
14155
|
+
safeSetInterval("worktree-sync", syncDivergedWorktrees, worktreeSyncIntervalMs);
|
|
14156
|
+
|
|
14065
14157
|
// ── Periodic epic branch sync/merge: every 15 min ──────────────────────────
|
|
14066
14158
|
const epicMergeIntervalMs = 15 * 60 * 1000;
|
|
14067
14159
|
safeSetInterval("epic-merge-check", () => checkEpicBranches("interval"), epicMergeIntervalMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.13",
|
|
4
4
|
"description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, 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",
|