bosun 0.34.8 → 0.34.9
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 +181 -116
- package/maintenance.mjs +102 -18
- package/monitor.mjs +34 -3
- package/package.json +1 -1
- package/setup-web-server.mjs +202 -0
- package/setup.mjs +4 -0
- package/task-store.mjs +19 -1
- package/ui/components/forms.js +6 -0
- package/ui/setup.html +1337 -112
- package/ui/tabs/agents.js +61 -49
- package/ui/tabs/workflows.js +18 -2
- package/ui-server.mjs +107 -13
- package/update-check.mjs +54 -0
- package/workflow-engine.mjs +51 -10
- package/workflow-nodes.mjs +400 -4
- package/workflow-templates/agents.mjs +32 -4
- package/workflow-templates/github.mjs +102 -36
- package/workflow-templates/reliability.mjs +37 -3
- package/workflow-templates.mjs +181 -10
package/cli.mjs
CHANGED
|
@@ -34,6 +34,15 @@ import {
|
|
|
34
34
|
migrateFromLegacy,
|
|
35
35
|
} from "./compat.mjs";
|
|
36
36
|
|
|
37
|
+
const MONITOR_START_MAX_WAIT_MS = Math.max(
|
|
38
|
+
0,
|
|
39
|
+
Number(process.env.BOSUN_MONITOR_START_MAX_WAIT_MS || "15000") || 15000,
|
|
40
|
+
);
|
|
41
|
+
const MONITOR_START_RETRY_MS = Math.max(
|
|
42
|
+
100,
|
|
43
|
+
Number(process.env.BOSUN_MONITOR_START_RETRY_MS || "500") || 500,
|
|
44
|
+
);
|
|
45
|
+
|
|
37
46
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
47
|
const args = process.argv.slice(2);
|
|
39
48
|
|
|
@@ -1484,133 +1493,189 @@ function detectExistingMonitorLockOwner(excludePid = null) {
|
|
|
1484
1493
|
return null;
|
|
1485
1494
|
}
|
|
1486
1495
|
|
|
1496
|
+
function getRequiredMonitorRuntimeFiles(monitorPath) {
|
|
1497
|
+
const required = [monitorPath];
|
|
1498
|
+
const copilotDir = resolve(
|
|
1499
|
+
__dirname,
|
|
1500
|
+
"node_modules",
|
|
1501
|
+
"@github",
|
|
1502
|
+
"copilot",
|
|
1503
|
+
);
|
|
1504
|
+
const conptyAgentPath = resolve(copilotDir, "conpty_console_list_agent.js");
|
|
1505
|
+
if (process.platform === "win32" && existsSync(copilotDir)) {
|
|
1506
|
+
required.push(conptyAgentPath);
|
|
1507
|
+
}
|
|
1508
|
+
return required;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function listMissingFiles(paths) {
|
|
1512
|
+
return paths.filter((entry) => !existsSync(entry));
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async function waitForMonitorRuntimeFiles(monitorPath) {
|
|
1516
|
+
const required = getRequiredMonitorRuntimeFiles(monitorPath);
|
|
1517
|
+
const startedAt = Date.now();
|
|
1518
|
+
let missing = listMissingFiles(required);
|
|
1519
|
+
while (
|
|
1520
|
+
missing.length > 0 &&
|
|
1521
|
+
Date.now() - startedAt < MONITOR_START_MAX_WAIT_MS
|
|
1522
|
+
) {
|
|
1523
|
+
await new Promise((resolveWait) => {
|
|
1524
|
+
setTimeout(resolveWait, MONITOR_START_RETRY_MS);
|
|
1525
|
+
});
|
|
1526
|
+
missing = listMissingFiles(required);
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
ready: missing.length === 0,
|
|
1530
|
+
missing,
|
|
1531
|
+
waitedMs: Date.now() - startedAt,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1487
1535
|
function runMonitor({ restartReason = "" } = {}) {
|
|
1488
1536
|
return new Promise((resolve, reject) => {
|
|
1489
1537
|
const monitorPath = fileURLToPath(
|
|
1490
1538
|
new URL("./monitor.mjs", import.meta.url),
|
|
1491
1539
|
);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
monitorChild = fork(monitorPath, process.argv.slice(2), {
|
|
1499
|
-
stdio: "inherit",
|
|
1500
|
-
execArgv: ["--max-old-space-size=4096"],
|
|
1501
|
-
env: childEnv,
|
|
1502
|
-
windowsHide: IS_DAEMON_CHILD && process.platform === "win32",
|
|
1503
|
-
});
|
|
1504
|
-
daemonCrashTracker.markStart();
|
|
1505
|
-
|
|
1506
|
-
monitorChild.on("exit", (code, signal) => {
|
|
1507
|
-
const childPid = monitorChild?.pid ?? null;
|
|
1508
|
-
monitorChild = null;
|
|
1509
|
-
if (code === SELF_RESTART_EXIT_CODE) {
|
|
1510
|
-
console.log(
|
|
1511
|
-
"\n \u21BB Monitor restarting with fresh modules...\n",
|
|
1512
|
-
);
|
|
1513
|
-
// Small delay to let file writes / port releases settle
|
|
1514
|
-
setTimeout(() => resolve(runMonitor({ restartReason: "self-restart" })), 2000);
|
|
1515
|
-
} else {
|
|
1516
|
-
const exitCode = code ?? (signal ? 1 : 0);
|
|
1517
|
-
const existingOwner =
|
|
1518
|
-
!gracefulShutdown && exitCode === 1
|
|
1519
|
-
? detectExistingMonitorLockOwner(childPid)
|
|
1520
|
-
: null;
|
|
1521
|
-
if (existingOwner) {
|
|
1522
|
-
console.log(
|
|
1523
|
-
`\n bosun is already running (PID ${existingOwner.pid}); exiting duplicate start.\n`,
|
|
1540
|
+
waitForMonitorRuntimeFiles(monitorPath)
|
|
1541
|
+
.then(({ ready, missing, waitedMs }) => {
|
|
1542
|
+
if (!ready) {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
`monitor runtime files missing after waiting ${Math.round(waitedMs / 1000)}s: ${missing.join(", ")}`,
|
|
1524
1545
|
);
|
|
1525
|
-
process.exit(0);
|
|
1526
|
-
return;
|
|
1527
1546
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
!gracefulShutdown &&
|
|
1532
|
-
(isOSKill || (IS_DAEMON_CHILD && exitCode !== 0));
|
|
1533
|
-
if (shouldAutoRestart) {
|
|
1534
|
-
const crashState = daemonCrashTracker.recordExit();
|
|
1535
|
-
daemonRestartCount += 1;
|
|
1536
|
-
const delayMs = isOSKill ? 5000 : DAEMON_RESTART_DELAY_MS;
|
|
1537
|
-
if (IS_DAEMON_CHILD && crashState.exceeded) {
|
|
1538
|
-
const durationSec = Math.max(
|
|
1539
|
-
1,
|
|
1540
|
-
Math.round(crashState.runDurationMs / 1000),
|
|
1541
|
-
);
|
|
1542
|
-
const windowSec = Math.max(
|
|
1543
|
-
1,
|
|
1544
|
-
Math.round(crashState.instantCrashWindowMs / 1000),
|
|
1545
|
-
);
|
|
1546
|
-
console.error(
|
|
1547
|
-
`\n ✖ Monitor crashed too quickly ${crashState.instantCrashCount} times in a row (each <= ${windowSec}s, latest ${durationSec}s). Auto-restart is now paused.`,
|
|
1548
|
-
);
|
|
1549
|
-
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1550
|
-
process.exit(exitCode),
|
|
1551
|
-
);
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
if (
|
|
1555
|
-
IS_DAEMON_CHILD &&
|
|
1556
|
-
DAEMON_MAX_RESTARTS > 0 &&
|
|
1557
|
-
daemonRestartCount > DAEMON_MAX_RESTARTS
|
|
1558
|
-
) {
|
|
1559
|
-
console.error(
|
|
1560
|
-
`\n ✖ Monitor crashed too many times (${daemonRestartCount - 1} restarts, max ${DAEMON_MAX_RESTARTS}).`,
|
|
1561
|
-
);
|
|
1562
|
-
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1563
|
-
process.exit(exitCode),
|
|
1564
|
-
);
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
const reasonLabel = signal
|
|
1568
|
-
? `signal ${signal}`
|
|
1569
|
-
: `exit code ${exitCode}`;
|
|
1570
|
-
const attemptLabel =
|
|
1571
|
-
IS_DAEMON_CHILD && DAEMON_MAX_RESTARTS > 0
|
|
1572
|
-
? `${daemonRestartCount}/${DAEMON_MAX_RESTARTS}`
|
|
1573
|
-
: `${daemonRestartCount}`;
|
|
1574
|
-
console.error(
|
|
1575
|
-
`\n ⚠ Monitor exited (${reasonLabel}) — auto-restarting in ${Math.max(1, Math.round(delayMs / 1000))}s${IS_DAEMON_CHILD ? ` [attempt ${attemptLabel}]` : ""}...`,
|
|
1547
|
+
if (waitedMs >= MONITOR_START_RETRY_MS) {
|
|
1548
|
+
console.warn(
|
|
1549
|
+
`[cli] delayed monitor start by ${Math.round(waitedMs / 1000)}s while waiting for runtime files to settle`,
|
|
1576
1550
|
);
|
|
1577
|
-
sendCrashNotification(exitCode, signal, {
|
|
1578
|
-
autoRestartInMs: delayMs,
|
|
1579
|
-
restartAttempt: daemonRestartCount,
|
|
1580
|
-
maxRestarts: IS_DAEMON_CHILD ? DAEMON_MAX_RESTARTS : 0,
|
|
1581
|
-
}).catch(() => {});
|
|
1582
|
-
setTimeout(
|
|
1583
|
-
() =>
|
|
1584
|
-
resolve(
|
|
1585
|
-
runMonitor({
|
|
1586
|
-
restartReason: isOSKill ? "os-kill" : "crash",
|
|
1587
|
-
}),
|
|
1588
|
-
),
|
|
1589
|
-
delayMs,
|
|
1590
|
-
);
|
|
1591
|
-
return;
|
|
1592
1551
|
}
|
|
1593
|
-
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
`\n ✖ Monitor crashed (${signal ? `signal ${signal}` : `exit code ${exitCode}`}) — sending crash notification...`,
|
|
1597
|
-
);
|
|
1598
|
-
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1599
|
-
process.exit(exitCode),
|
|
1600
|
-
);
|
|
1552
|
+
const childEnv = { ...process.env };
|
|
1553
|
+
if (restartReason) {
|
|
1554
|
+
childEnv.BOSUN_MONITOR_RESTART_REASON = restartReason;
|
|
1601
1555
|
} else {
|
|
1602
|
-
|
|
1603
|
-
daemonCrashTracker.reset();
|
|
1604
|
-
process.exit(exitCode);
|
|
1556
|
+
delete childEnv.BOSUN_MONITOR_RESTART_REASON;
|
|
1605
1557
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1558
|
+
monitorChild = fork(monitorPath, process.argv.slice(2), {
|
|
1559
|
+
stdio: "inherit",
|
|
1560
|
+
execArgv: ["--max-old-space-size=4096"],
|
|
1561
|
+
env: childEnv,
|
|
1562
|
+
windowsHide: IS_DAEMON_CHILD && process.platform === "win32",
|
|
1563
|
+
});
|
|
1564
|
+
daemonCrashTracker.markStart();
|
|
1565
|
+
|
|
1566
|
+
monitorChild.on("exit", (code, signal) => {
|
|
1567
|
+
const childPid = monitorChild?.pid ?? null;
|
|
1568
|
+
monitorChild = null;
|
|
1569
|
+
if (code === SELF_RESTART_EXIT_CODE) {
|
|
1570
|
+
console.log(
|
|
1571
|
+
"\n ↻ Monitor restarting with fresh modules...\n",
|
|
1572
|
+
);
|
|
1573
|
+
// Small delay to let file writes / port releases settle
|
|
1574
|
+
setTimeout(() => resolve(runMonitor({ restartReason: "self-restart" })), 2000);
|
|
1575
|
+
} else {
|
|
1576
|
+
const exitCode = code ?? (signal ? 1 : 0);
|
|
1577
|
+
const existingOwner =
|
|
1578
|
+
!gracefulShutdown && exitCode === 1
|
|
1579
|
+
? detectExistingMonitorLockOwner(childPid)
|
|
1580
|
+
: null;
|
|
1581
|
+
if (existingOwner) {
|
|
1582
|
+
console.log(
|
|
1583
|
+
`\n bosun is already running (PID ${existingOwner.pid}); exiting duplicate start.\n`,
|
|
1584
|
+
);
|
|
1585
|
+
process.exit(0);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
// 4294967295 (0xFFFFFFFF / -1 signed) = OS killed the process (OOM, external termination)
|
|
1589
|
+
const isOSKill = exitCode === 4294967295 || exitCode === -1;
|
|
1590
|
+
const shouldAutoRestart =
|
|
1591
|
+
!gracefulShutdown &&
|
|
1592
|
+
(isOSKill || (IS_DAEMON_CHILD && exitCode !== 0));
|
|
1593
|
+
if (shouldAutoRestart) {
|
|
1594
|
+
const crashState = daemonCrashTracker.recordExit();
|
|
1595
|
+
daemonRestartCount += 1;
|
|
1596
|
+
const delayMs = isOSKill ? 5000 : DAEMON_RESTART_DELAY_MS;
|
|
1597
|
+
if (IS_DAEMON_CHILD && crashState.exceeded) {
|
|
1598
|
+
const durationSec = Math.max(
|
|
1599
|
+
1,
|
|
1600
|
+
Math.round(crashState.runDurationMs / 1000),
|
|
1601
|
+
);
|
|
1602
|
+
const windowSec = Math.max(
|
|
1603
|
+
1,
|
|
1604
|
+
Math.round(crashState.instantCrashWindowMs / 1000),
|
|
1605
|
+
);
|
|
1606
|
+
console.error(
|
|
1607
|
+
`\n ✖ Monitor crashed too quickly ${crashState.instantCrashCount} times in a row (each <= ${windowSec}s, latest ${durationSec}s). Auto-restart is now paused.`,
|
|
1608
|
+
);
|
|
1609
|
+
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1610
|
+
process.exit(exitCode),
|
|
1611
|
+
);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (
|
|
1615
|
+
IS_DAEMON_CHILD &&
|
|
1616
|
+
DAEMON_MAX_RESTARTS > 0 &&
|
|
1617
|
+
daemonRestartCount > DAEMON_MAX_RESTARTS
|
|
1618
|
+
) {
|
|
1619
|
+
console.error(
|
|
1620
|
+
`\n ✖ Monitor crashed too many times (${daemonRestartCount - 1} restarts, max ${DAEMON_MAX_RESTARTS}).`,
|
|
1621
|
+
);
|
|
1622
|
+
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1623
|
+
process.exit(exitCode),
|
|
1624
|
+
);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const reasonLabel = signal
|
|
1628
|
+
? `signal ${signal}`
|
|
1629
|
+
: `exit code ${exitCode}`;
|
|
1630
|
+
const attemptLabel =
|
|
1631
|
+
IS_DAEMON_CHILD && DAEMON_MAX_RESTARTS > 0
|
|
1632
|
+
? `${daemonRestartCount}/${DAEMON_MAX_RESTARTS}`
|
|
1633
|
+
: `${daemonRestartCount}`;
|
|
1634
|
+
console.error(
|
|
1635
|
+
`\n ⚠ Monitor exited (${reasonLabel}) — auto-restarting in ${Math.max(1, Math.round(delayMs / 1000))}s${IS_DAEMON_CHILD ? ` [attempt ${attemptLabel}]` : ""}...`,
|
|
1636
|
+
);
|
|
1637
|
+
sendCrashNotification(exitCode, signal, {
|
|
1638
|
+
autoRestartInMs: delayMs,
|
|
1639
|
+
restartAttempt: daemonRestartCount,
|
|
1640
|
+
maxRestarts: IS_DAEMON_CHILD ? DAEMON_MAX_RESTARTS : 0,
|
|
1641
|
+
}).catch(() => {});
|
|
1642
|
+
setTimeout(
|
|
1643
|
+
() =>
|
|
1644
|
+
resolve(
|
|
1645
|
+
runMonitor({
|
|
1646
|
+
restartReason: isOSKill ? "os-kill" : "crash",
|
|
1647
|
+
}),
|
|
1648
|
+
),
|
|
1649
|
+
delayMs,
|
|
1650
|
+
);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (exitCode !== 0 && !gracefulShutdown) {
|
|
1655
|
+
console.error(
|
|
1656
|
+
`\n ✖ Monitor crashed (${signal ? `signal ${signal}` : `exit code ${exitCode}`}) — sending crash notification...`,
|
|
1657
|
+
);
|
|
1658
|
+
sendCrashNotification(exitCode, signal).finally(() =>
|
|
1659
|
+
process.exit(exitCode),
|
|
1660
|
+
);
|
|
1661
|
+
} else {
|
|
1662
|
+
daemonRestartCount = 0;
|
|
1663
|
+
daemonCrashTracker.reset();
|
|
1664
|
+
process.exit(exitCode);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
monitorChild.on("error", (err) => {
|
|
1670
|
+
monitorChild = null;
|
|
1671
|
+
console.error(`\n ✖ Monitor failed to start: ${err.message}`);
|
|
1672
|
+
sendCrashNotification(1, null).finally(() => reject(err));
|
|
1673
|
+
});
|
|
1674
|
+
})
|
|
1675
|
+
.catch((err) => {
|
|
1676
|
+
console.error(`\n ✖ Monitor failed to start: ${err.message}`);
|
|
1677
|
+
sendCrashNotification(1, null).finally(() => reject(err));
|
|
1678
|
+
});
|
|
1614
1679
|
});
|
|
1615
1680
|
}
|
|
1616
1681
|
|
package/maintenance.mjs
CHANGED
|
@@ -28,6 +28,56 @@ import {
|
|
|
28
28
|
} from "./worktree-manager.mjs";
|
|
29
29
|
|
|
30
30
|
const isWindows = process.platform === "win32";
|
|
31
|
+
const BRANCH_SYNC_LOG_THROTTLE_MS = Math.max(
|
|
32
|
+
5_000,
|
|
33
|
+
Number(process.env.BRANCH_SYNC_LOG_THROTTLE_MS || "300000") || 300000,
|
|
34
|
+
);
|
|
35
|
+
const branchSyncLogState = new Map();
|
|
36
|
+
|
|
37
|
+
function logThrottledBranchSync(
|
|
38
|
+
key,
|
|
39
|
+
message,
|
|
40
|
+
level = "warn",
|
|
41
|
+
throttleMs = BRANCH_SYNC_LOG_THROTTLE_MS,
|
|
42
|
+
) {
|
|
43
|
+
const normalizedKey = String(key || "default").trim() || "default";
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const state = branchSyncLogState.get(normalizedKey) || {
|
|
46
|
+
lastLoggedAt: 0,
|
|
47
|
+
suppressed: 0,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
state.lastLoggedAt > 0 &&
|
|
52
|
+
now - state.lastLoggedAt < Math.max(1_000, Number(throttleMs) || BRANCH_SYNC_LOG_THROTTLE_MS)
|
|
53
|
+
) {
|
|
54
|
+
state.suppressed += 1;
|
|
55
|
+
branchSyncLogState.set(normalizedKey, state);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const suppressed = state.suppressed || 0;
|
|
60
|
+
const suffix =
|
|
61
|
+
suppressed > 0
|
|
62
|
+
? ` (suppressed ${suppressed} similar message(s) in last ${Math.round(Math.max(1_000, Number(throttleMs) || BRANCH_SYNC_LOG_THROTTLE_MS) / 1000)}s)`
|
|
63
|
+
: "";
|
|
64
|
+
const line = `${message}${suffix}`;
|
|
65
|
+
|
|
66
|
+
if (level === "error") {
|
|
67
|
+
console.error(line);
|
|
68
|
+
} else if (level === "info") {
|
|
69
|
+
console.info(line);
|
|
70
|
+
} else if (level === "log") {
|
|
71
|
+
console.log(line);
|
|
72
|
+
} else {
|
|
73
|
+
console.warn(line);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
branchSyncLogState.set(normalizedKey, {
|
|
77
|
+
lastLoggedAt: now,
|
|
78
|
+
suppressed: 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
31
81
|
|
|
32
82
|
/**
|
|
33
83
|
* Get all running processes matching a filter.
|
|
@@ -826,10 +876,10 @@ function isProcessAlive(pid) {
|
|
|
826
876
|
* branches spawned from it start stale, causing avoidable rebase conflicts.
|
|
827
877
|
* This function periodically pulls so the local ref stays current.
|
|
828
878
|
*
|
|
829
|
-
* Safe: only does `--ff-only` — never creates merge commits.
|
|
830
|
-
* branch has
|
|
831
|
-
*
|
|
832
|
-
*
|
|
879
|
+
* Safe: only does `--ff-only` — never creates merge commits. It skips when
|
|
880
|
+
* the checked-out branch has uncommitted changes (git will refuse the checkout
|
|
881
|
+
* anyway). If local and remote both have unique commits, it logs a warning and
|
|
882
|
+
* skips.
|
|
833
883
|
*
|
|
834
884
|
* @param {string} repoRoot
|
|
835
885
|
* @param {string[]} [branches] - branches to sync (default: ["main"])
|
|
@@ -848,7 +898,11 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
848
898
|
windowsHide: true,
|
|
849
899
|
});
|
|
850
900
|
} catch (e) {
|
|
851
|
-
|
|
901
|
+
logThrottledBranchSync(
|
|
902
|
+
"sync:fetch-failed",
|
|
903
|
+
`[maintenance] git fetch --all failed: ${e.message}`,
|
|
904
|
+
"warn",
|
|
905
|
+
);
|
|
852
906
|
return 0;
|
|
853
907
|
}
|
|
854
908
|
|
|
@@ -913,13 +967,17 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
913
967
|
{ cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
|
|
914
968
|
);
|
|
915
969
|
if (push.status === 0) {
|
|
916
|
-
|
|
970
|
+
logThrottledBranchSync(
|
|
971
|
+
`sync:${branch}:push-success`,
|
|
917
972
|
`[maintenance] pushed local '${branch}' to origin (${ahead} commit(s) ahead)`,
|
|
973
|
+
"info",
|
|
918
974
|
);
|
|
919
975
|
synced++;
|
|
920
976
|
} else {
|
|
921
|
-
|
|
977
|
+
logThrottledBranchSync(
|
|
978
|
+
`sync:${branch}:push-failed`,
|
|
922
979
|
`[maintenance] git push '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
|
|
980
|
+
"warn",
|
|
923
981
|
);
|
|
924
982
|
}
|
|
925
983
|
continue;
|
|
@@ -935,8 +993,10 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
935
993
|
windowsHide: true,
|
|
936
994
|
});
|
|
937
995
|
if (statusCheck.stdout?.trim()) {
|
|
938
|
-
|
|
996
|
+
logThrottledBranchSync(
|
|
997
|
+
`sync:${branch}:diverged-dirty`,
|
|
939
998
|
`[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) but has uncommitted changes — skipping`,
|
|
999
|
+
"info",
|
|
940
1000
|
);
|
|
941
1001
|
continue;
|
|
942
1002
|
}
|
|
@@ -961,18 +1021,24 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
961
1021
|
{ cwd: repoRoot, encoding: "utf8", timeout: 30_000, windowsHide: true },
|
|
962
1022
|
);
|
|
963
1023
|
if (push.status === 0) {
|
|
964
|
-
|
|
1024
|
+
logThrottledBranchSync(
|
|
1025
|
+
`sync:${branch}:rebase-push-success`,
|
|
965
1026
|
`[maintenance] rebased and pushed '${branch}' (was ${ahead}↑ ${behind}↓)`,
|
|
1027
|
+
"info",
|
|
966
1028
|
);
|
|
967
1029
|
synced++;
|
|
968
1030
|
} else {
|
|
969
|
-
|
|
1031
|
+
logThrottledBranchSync(
|
|
1032
|
+
`sync:${branch}:rebase-push-failed`,
|
|
970
1033
|
`[maintenance] push after rebase of '${branch}' failed: ${(push.stderr || push.stdout || "").toString().trim()}`,
|
|
1034
|
+
"warn",
|
|
971
1035
|
);
|
|
972
1036
|
}
|
|
973
1037
|
} else {
|
|
974
|
-
|
|
1038
|
+
logThrottledBranchSync(
|
|
1039
|
+
`sync:${branch}:diverged-not-checked-out`,
|
|
975
1040
|
`[maintenance] local '${branch}' diverged (${ahead}↑ ${behind}↓) and not checked out — skipping (rebase requires checkout)`,
|
|
1041
|
+
"warn",
|
|
976
1042
|
);
|
|
977
1043
|
}
|
|
978
1044
|
continue;
|
|
@@ -988,8 +1054,10 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
988
1054
|
windowsHide: true,
|
|
989
1055
|
});
|
|
990
1056
|
if (statusCheck.stdout?.trim()) {
|
|
991
|
-
|
|
1057
|
+
logThrottledBranchSync(
|
|
1058
|
+
`sync:${branch}:dirty-pull-skip`,
|
|
992
1059
|
`[maintenance] '${branch}' is checked out with uncommitted changes — skipping pull`,
|
|
1060
|
+
"info",
|
|
993
1061
|
);
|
|
994
1062
|
continue;
|
|
995
1063
|
}
|
|
@@ -1001,13 +1069,17 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
1001
1069
|
windowsHide: true,
|
|
1002
1070
|
});
|
|
1003
1071
|
if (pull.status === 0) {
|
|
1004
|
-
|
|
1072
|
+
logThrottledBranchSync(
|
|
1073
|
+
`sync:${branch}:pull-success`,
|
|
1005
1074
|
`[maintenance] fast-forwarded checked-out '${branch}' (was ${behind} behind)`,
|
|
1075
|
+
"info",
|
|
1006
1076
|
);
|
|
1007
1077
|
synced++;
|
|
1008
1078
|
} else {
|
|
1009
|
-
|
|
1079
|
+
logThrottledBranchSync(
|
|
1080
|
+
`sync:${branch}:pull-failed`,
|
|
1010
1081
|
`[maintenance] git pull --ff-only on '${branch}' failed: ${(pull.stderr || pull.stdout || "").toString().trim()}`,
|
|
1082
|
+
"warn",
|
|
1011
1083
|
);
|
|
1012
1084
|
}
|
|
1013
1085
|
} else {
|
|
@@ -1019,22 +1091,34 @@ export function syncLocalTrackingBranches(repoRoot, branches) {
|
|
|
1019
1091
|
{ cwd: repoRoot, timeout: 5000, windowsHide: true },
|
|
1020
1092
|
);
|
|
1021
1093
|
if (update.status === 0) {
|
|
1022
|
-
|
|
1094
|
+
logThrottledBranchSync(
|
|
1095
|
+
`sync:${branch}:update-ref-success`,
|
|
1023
1096
|
`[maintenance] fast-forwarded '${branch}' → ${remoteRef} (was ${behind} behind)`,
|
|
1097
|
+
"info",
|
|
1024
1098
|
);
|
|
1025
1099
|
synced++;
|
|
1026
1100
|
} else {
|
|
1027
|
-
|
|
1101
|
+
logThrottledBranchSync(
|
|
1102
|
+
`sync:${branch}:update-ref-failed`,
|
|
1103
|
+
`[maintenance] update-ref failed for '${branch}'`,
|
|
1104
|
+
"warn",
|
|
1105
|
+
);
|
|
1028
1106
|
}
|
|
1029
1107
|
}
|
|
1030
1108
|
} catch (e) {
|
|
1031
|
-
|
|
1109
|
+
logThrottledBranchSync(
|
|
1110
|
+
`sync:${branch}:unexpected-error`,
|
|
1111
|
+
`[maintenance] error syncing '${branch}': ${e.message}`,
|
|
1112
|
+
"error",
|
|
1113
|
+
);
|
|
1032
1114
|
}
|
|
1033
1115
|
}
|
|
1034
1116
|
|
|
1035
1117
|
if (synced > 0) {
|
|
1036
|
-
|
|
1118
|
+
logThrottledBranchSync(
|
|
1119
|
+
"sync:summary",
|
|
1037
1120
|
`[maintenance] synced ${synced}/${toSync.length} local tracking branch(es)`,
|
|
1121
|
+
"info",
|
|
1038
1122
|
);
|
|
1039
1123
|
}
|
|
1040
1124
|
return synced;
|
package/monitor.mjs
CHANGED
|
@@ -1457,10 +1457,27 @@ function isMonitorMonitorEnabled() {
|
|
|
1457
1457
|
}
|
|
1458
1458
|
|
|
1459
1459
|
function isSelfRestartWatcherEnabled() {
|
|
1460
|
+
const devMode = isDevMode();
|
|
1461
|
+
const force = process.env.SELF_RESTART_WATCH_FORCE;
|
|
1462
|
+
const forceEnabled = isTruthyFlag(force);
|
|
1463
|
+
const npmLifecycleEvent = String(process.env.npm_lifecycle_event || "")
|
|
1464
|
+
.trim()
|
|
1465
|
+
.toLowerCase();
|
|
1466
|
+
const launchedViaNpmStartScript =
|
|
1467
|
+
npmLifecycleEvent === "start" || npmLifecycleEvent.startsWith("start:");
|
|
1460
1468
|
const explicit = process.env.SELF_RESTART_WATCH_ENABLED;
|
|
1461
1469
|
if (explicit !== undefined && String(explicit).trim() !== "") {
|
|
1462
1470
|
return !isFalsyFlag(explicit);
|
|
1463
1471
|
}
|
|
1472
|
+
if (!devMode && !forceEnabled) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
if (devMode && !forceEnabled && !launchedViaNpmStartScript) {
|
|
1476
|
+
// Plain `bosun` command launches from a source checkout should behave like
|
|
1477
|
+
// npm/prod installs by default: no self-restart watcher unless explicitly
|
|
1478
|
+
// enabled. Auto-updates still handle published package changes.
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1464
1481
|
if (
|
|
1465
1482
|
String(executorMode || "")
|
|
1466
1483
|
.trim()
|
|
@@ -1476,7 +1493,7 @@ function isSelfRestartWatcherEnabled() {
|
|
|
1476
1493
|
// Dev mode (source checkout / monorepo) → watch for code changes.
|
|
1477
1494
|
// npm mode (installed via npm) → do NOT watch; source only changes via
|
|
1478
1495
|
// npm update, which is handled by the auto-update loop instead.
|
|
1479
|
-
return
|
|
1496
|
+
return devMode;
|
|
1480
1497
|
}
|
|
1481
1498
|
|
|
1482
1499
|
const MONITOR_MONITOR_DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000;
|
|
@@ -14532,7 +14549,7 @@ function applyConfig(nextConfig, options = {}) {
|
|
|
14532
14549
|
} else {
|
|
14533
14550
|
stopSelfWatcher();
|
|
14534
14551
|
console.log(
|
|
14535
|
-
"[monitor] self-restart watcher disabled (set
|
|
14552
|
+
"[monitor] self-restart watcher disabled (set SELF_RESTART_WATCH_FORCE=1 to allow in npm/prod mode)",
|
|
14536
14553
|
);
|
|
14537
14554
|
}
|
|
14538
14555
|
}
|
|
@@ -15136,8 +15153,22 @@ if (selfRestartWatcherEnabled) {
|
|
|
15136
15153
|
const normalizedExecutorMode = String(executorMode || "")
|
|
15137
15154
|
.trim()
|
|
15138
15155
|
.toLowerCase();
|
|
15139
|
-
const
|
|
15156
|
+
const explicitSelfRestartWatch = process.env.SELF_RESTART_WATCH_ENABLED;
|
|
15157
|
+
const hasExplicitSelfRestartWatch =
|
|
15158
|
+
explicitSelfRestartWatch !== undefined &&
|
|
15159
|
+
String(explicitSelfRestartWatch).trim() !== "";
|
|
15160
|
+
const forceSelfRestartWatch = isTruthyFlag(process.env.SELF_RESTART_WATCH_FORCE);
|
|
15161
|
+
const npmLifecycleEvent = String(process.env.npm_lifecycle_event || "")
|
|
15162
|
+
.trim()
|
|
15163
|
+
.toLowerCase();
|
|
15164
|
+
const launchedViaNpmStartScript =
|
|
15165
|
+
npmLifecycleEvent === "start" || npmLifecycleEvent.startsWith("start:");
|
|
15166
|
+
const disabledReason = hasExplicitSelfRestartWatch
|
|
15167
|
+
? "explicitly"
|
|
15168
|
+
: !isDevMode()
|
|
15140
15169
|
? "npm/prod mode — updates via auto-update loop"
|
|
15170
|
+
: !forceSelfRestartWatch && !launchedViaNpmStartScript
|
|
15171
|
+
? "CLI command mode in source checkout — use npm run start or SELF_RESTART_WATCH_ENABLED=1 to enable"
|
|
15141
15172
|
: normalizedExecutorMode === "internal" || normalizedExecutorMode === "hybrid"
|
|
15142
15173
|
? `executor mode "${normalizedExecutorMode}" (continuous task-driven code changes)`
|
|
15143
15174
|
: "explicitly";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.9",
|
|
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",
|