bosun 0.40.2 → 0.40.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/README.md +4 -0
- package/cli.mjs +127 -69
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +52 -16
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +16 -10
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +1 -1
- package/server/ui-server.mjs +502 -39
- package/task/task-executor.mjs +690 -6
- package/task/task-store.mjs +116 -1
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/demo.html +26 -1
- package/ui/modules/mui.js +600 -397
- package/ui/styles/components.css +43 -3
- package/ui/styles/kanban.css +66 -11
- package/ui/styles.monolith.css +89 -0
- package/ui/tabs/agents.js +194 -20
- package/ui/tabs/tasks.js +673 -162
- package/workflow/workflow-engine.mjs +30 -29
- package/workflow/workflow-nodes.mjs +321 -22
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +25 -0
- package/workspace/worktree-manager.mjs +8 -2
package/README.md
CHANGED
|
@@ -181,6 +181,10 @@ npm run hooks:install
|
|
|
181
181
|
- `docs/` and `_docs/` — product docs, deep technical references, and long-form source material
|
|
182
182
|
- `tools/` and `tests/` — build utilities, release checks, and regression coverage
|
|
183
183
|
|
|
184
|
+
If you find this project useful or would like to stay up to date with new releases, a star is appreciated!
|
|
185
|
+
|
|
186
|
+
[](https://www.star-history.com/?repos=VirtEngine%2FBosun&type=date&legend=top-left)
|
|
187
|
+
|
|
184
188
|
---
|
|
185
189
|
|
|
186
190
|
## License
|
package/cli.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 3. Configuration loading from config.mjs
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { resolve, dirname } from "node:path";
|
|
19
|
+
import { isAbsolute, resolve, dirname } from "node:path";
|
|
20
20
|
import {
|
|
21
21
|
existsSync,
|
|
22
22
|
readFileSync,
|
|
@@ -237,6 +237,9 @@ function isWslInteropRuntime() {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function resolveConfigDirForCli() {
|
|
240
|
+
const configDirArg = getArgValue("--config-dir");
|
|
241
|
+
if (configDirArg) return resolve(configDirArg);
|
|
242
|
+
if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
|
|
240
243
|
if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
|
|
241
244
|
const preferWindowsDirs =
|
|
242
245
|
process.platform === "win32" && !isWslInteropRuntime();
|
|
@@ -612,6 +615,40 @@ function removePidFile() {
|
|
|
612
615
|
}
|
|
613
616
|
}
|
|
614
617
|
|
|
618
|
+
function absolutizeDaemonArgPath(value) {
|
|
619
|
+
const raw = String(value || "").trim();
|
|
620
|
+
if (!raw) return raw;
|
|
621
|
+
return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function normalizeDetachedDaemonArgs(rawArgs = []) {
|
|
625
|
+
const normalized = Array.isArray(rawArgs) ? [...rawArgs] : [];
|
|
626
|
+
const pathFlags = new Set(["--config-dir", "--repo-root", "--log-dir"]);
|
|
627
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
628
|
+
const arg = String(normalized[i] || "").trim();
|
|
629
|
+
if (!arg.startsWith("--")) continue;
|
|
630
|
+
|
|
631
|
+
const eq = arg.indexOf("=");
|
|
632
|
+
if (eq > 0) {
|
|
633
|
+
const flag = arg.slice(0, eq);
|
|
634
|
+
const value = arg.slice(eq + 1);
|
|
635
|
+
if (pathFlags.has(flag)) {
|
|
636
|
+
normalized[i] = flag + "=" + absolutizeDaemonArgPath(value);
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (pathFlags.has(arg) && i + 1 < normalized.length) {
|
|
642
|
+
const value = String(normalized[i + 1] || "").trim();
|
|
643
|
+
if (value && !value.startsWith("--")) {
|
|
644
|
+
normalized[i + 1] = absolutizeDaemonArgPath(value);
|
|
645
|
+
i += 1;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return normalized;
|
|
650
|
+
}
|
|
651
|
+
|
|
615
652
|
function startDaemon() {
|
|
616
653
|
const existing = getDaemonPid();
|
|
617
654
|
if (existing) {
|
|
@@ -656,7 +693,9 @@ function startDaemon() {
|
|
|
656
693
|
...runAsNode,
|
|
657
694
|
"--max-old-space-size=4096",
|
|
658
695
|
fileURLToPath(new URL("./cli.mjs", import.meta.url)),
|
|
659
|
-
...
|
|
696
|
+
...normalizeDetachedDaemonArgs(
|
|
697
|
+
process.argv.slice(2).filter((a) => a !== "--daemon" && a !== "-d"),
|
|
698
|
+
),
|
|
660
699
|
"--daemon-child",
|
|
661
700
|
],
|
|
662
701
|
{
|
|
@@ -864,6 +903,46 @@ function findAllBosunProcessPids() {
|
|
|
864
903
|
}
|
|
865
904
|
}
|
|
866
905
|
|
|
906
|
+
function getRunningDaemonPids() {
|
|
907
|
+
const trackedDaemonPid = getDaemonPid();
|
|
908
|
+
if (trackedDaemonPid) return [trackedDaemonPid];
|
|
909
|
+
const ghostPids = findGhostDaemonPids();
|
|
910
|
+
return ghostPids.length > 0 ? ghostPids : [];
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function stopBosunProcesses(
|
|
914
|
+
pids,
|
|
915
|
+
{ reason = null, timeoutMs = 5000 } = {},
|
|
916
|
+
) {
|
|
917
|
+
const targets = Array.from(
|
|
918
|
+
new Set(
|
|
919
|
+
(Array.isArray(pids) ? pids : []).filter(
|
|
920
|
+
(pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid,
|
|
921
|
+
),
|
|
922
|
+
),
|
|
923
|
+
);
|
|
924
|
+
if (targets.length === 0) return [];
|
|
925
|
+
if (reason) {
|
|
926
|
+
console.log(` ${reason}: ${targets.join(", ")}`);
|
|
927
|
+
}
|
|
928
|
+
for (const pid of targets) {
|
|
929
|
+
try {
|
|
930
|
+
process.kill(pid, "SIGTERM");
|
|
931
|
+
} catch {
|
|
932
|
+
/* already dead */
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const alive = waitForPidsToExit(targets, timeoutMs);
|
|
936
|
+
for (const pid of alive) {
|
|
937
|
+
try {
|
|
938
|
+
process.kill(pid, "SIGKILL");
|
|
939
|
+
} catch {
|
|
940
|
+
/* already dead */
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return targets;
|
|
944
|
+
}
|
|
945
|
+
|
|
867
946
|
function removeKnownPidFiles(extraCacheDirs = []) {
|
|
868
947
|
const pidFiles = uniqueResolvedPaths([
|
|
869
948
|
...getPidFileCandidates("bosun-daemon.pid", extraCacheDirs),
|
|
@@ -1506,78 +1585,57 @@ async function main() {
|
|
|
1506
1585
|
|
|
1507
1586
|
// Handle --update (force update)
|
|
1508
1587
|
if (args.includes("--update")) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1588
|
+
const runningDaemonPids = getRunningDaemonPids();
|
|
1589
|
+
const shouldRestartDaemon = runningDaemonPids.length > 0;
|
|
1590
|
+
const runningSiblingPids = findAllBosunProcessPids().filter(
|
|
1591
|
+
(pid) => pid !== process.pid,
|
|
1592
|
+
);
|
|
1593
|
+
const runningNonDaemonPids = runningSiblingPids.filter(
|
|
1594
|
+
(pid) => !runningDaemonPids.includes(pid),
|
|
1595
|
+
);
|
|
1596
|
+
if (shouldRestartDaemon) {
|
|
1513
1597
|
console.log(
|
|
1514
|
-
|
|
1598
|
+
" Note: a successful update will restart the bosun daemon automatically.",
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
if (runningNonDaemonPids.length > 0) {
|
|
1602
|
+
console.log(
|
|
1603
|
+
` Note: ${runningNonDaemonPids.length} other running bosun process(es) will be stopped after a successful update.`,
|
|
1515
1604
|
);
|
|
1516
|
-
for (const pid of siblingPids) {
|
|
1517
|
-
try {
|
|
1518
|
-
process.kill(pid, "SIGTERM");
|
|
1519
|
-
} catch {
|
|
1520
|
-
/* already dead */
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
const deadline = Date.now() + 5000;
|
|
1524
|
-
let alive = siblingPids.filter((pid) => isProcessAlive(pid));
|
|
1525
|
-
while (alive.length > 0 && Date.now() < deadline) {
|
|
1526
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
1527
|
-
alive = alive.filter((pid) => isProcessAlive(pid));
|
|
1528
|
-
}
|
|
1529
|
-
for (const pid of alive) {
|
|
1530
|
-
try {
|
|
1531
|
-
process.kill(pid, "SIGKILL");
|
|
1532
|
-
} catch {
|
|
1533
|
-
/* already dead */
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
1605
|
}
|
|
1537
1606
|
|
|
1538
1607
|
const { forceUpdate } = await import("./infra/update-check.mjs");
|
|
1539
1608
|
const updated = await forceUpdate(VERSION);
|
|
1540
|
-
if (updated) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1609
|
+
if (!updated) {
|
|
1610
|
+
process.exit(0);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Only stop sibling processes once npm has actually installed the update.
|
|
1614
|
+
const daemonPids = getRunningDaemonPids();
|
|
1615
|
+
const siblingPids = Array.from(
|
|
1616
|
+
new Set([
|
|
1617
|
+
...findAllBosunProcessPids().filter((pid) => pid !== process.pid),
|
|
1618
|
+
...daemonPids,
|
|
1619
|
+
]),
|
|
1620
|
+
);
|
|
1621
|
+
stopBosunProcesses(siblingPids, {
|
|
1622
|
+
reason: `Stopping ${siblingPids.length} running bosun process(es) to finish update`,
|
|
1623
|
+
timeoutMs: 5000,
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
if (shouldRestartDaemon) {
|
|
1550
1627
|
if (daemonPids.length > 0) {
|
|
1551
|
-
console.log(
|
|
1552
|
-
` Daemon running (PID ${daemonPids.join(", ")}). Stopping for restart...`,
|
|
1553
|
-
);
|
|
1554
|
-
for (const p of daemonPids) {
|
|
1555
|
-
try {
|
|
1556
|
-
process.kill(p, "SIGTERM");
|
|
1557
|
-
} catch {
|
|
1558
|
-
/* already dead */
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
// Wait up to 4 s for graceful exit
|
|
1562
|
-
const deadline = Date.now() + 4000;
|
|
1563
|
-
let alive = daemonPids.filter((p) => isProcessAlive(p));
|
|
1564
|
-
while (alive.length > 0 && Date.now() < deadline) {
|
|
1565
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
1566
|
-
alive = alive.filter((p) => isProcessAlive(p));
|
|
1567
|
-
}
|
|
1568
|
-
for (const p of alive) {
|
|
1569
|
-
try {
|
|
1570
|
-
process.kill(p, "SIGKILL");
|
|
1571
|
-
} catch {
|
|
1572
|
-
/* ok */
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
1628
|
removePidFile();
|
|
1576
|
-
console.log(" Restarting daemon with updated version...");
|
|
1577
|
-
startDaemon(); // spawns new daemon-child and calls process.exit(0)
|
|
1578
|
-
return; // unreachable — startDaemon exits
|
|
1579
1629
|
}
|
|
1630
|
+
console.log(" Restarting daemon with updated version...");
|
|
1631
|
+
startDaemon(); // spawns new daemon-child and calls process.exit(0)
|
|
1632
|
+
return; // unreachable — startDaemon exits
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (siblingPids.length > 0) {
|
|
1636
|
+
console.log(" Restart stopped bosun sessions manually if you still need them.");
|
|
1580
1637
|
}
|
|
1638
|
+
console.log(" Restart bosun to use the new version.\n");
|
|
1581
1639
|
process.exit(0);
|
|
1582
1640
|
}
|
|
1583
1641
|
|
|
@@ -1610,7 +1668,7 @@ async function main() {
|
|
|
1610
1668
|
if (args.includes("--workspace-list") || args.includes("workspace-list")) {
|
|
1611
1669
|
const { listWorkspaces, getActiveWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1612
1670
|
const configDirArg = getArgValue("--config-dir");
|
|
1613
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1671
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1614
1672
|
const workspaces = listWorkspaces(configDir);
|
|
1615
1673
|
const active = getActiveWorkspace(configDir);
|
|
1616
1674
|
if (workspaces.length === 0) {
|
|
@@ -1634,7 +1692,7 @@ async function main() {
|
|
|
1634
1692
|
if (args.includes("--workspace-add")) {
|
|
1635
1693
|
const { createWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1636
1694
|
const configDirArg = getArgValue("--config-dir");
|
|
1637
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1695
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1638
1696
|
const name = getArgValue("--workspace-add");
|
|
1639
1697
|
if (!name) {
|
|
1640
1698
|
console.error(" Error: workspace name is required. Usage: bosun --workspace-add <name>");
|
|
@@ -1653,7 +1711,7 @@ async function main() {
|
|
|
1653
1711
|
if (args.includes("--workspace-switch")) {
|
|
1654
1712
|
const { setActiveWorkspace, getWorkspace } = await import("./workspace/workspace-manager.mjs");
|
|
1655
1713
|
const configDirArg = getArgValue("--config-dir");
|
|
1656
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1714
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1657
1715
|
const wsId = getArgValue("--workspace-switch");
|
|
1658
1716
|
if (!wsId) {
|
|
1659
1717
|
console.error(" Error: workspace ID required. Usage: bosun --workspace-switch <id>");
|
|
@@ -1673,7 +1731,7 @@ async function main() {
|
|
|
1673
1731
|
if (args.includes("--workspace-add-repo")) {
|
|
1674
1732
|
const { addRepoToWorkspace, getActiveWorkspace, listWorkspaces } = await import("./workspace/workspace-manager.mjs");
|
|
1675
1733
|
const configDirArg = getArgValue("--config-dir");
|
|
1676
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1734
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1677
1735
|
const active = getActiveWorkspace(configDir);
|
|
1678
1736
|
if (!active) {
|
|
1679
1737
|
console.error(" No active workspace. Create one first: bosun --workspace-add <name>");
|
|
@@ -1702,7 +1760,7 @@ async function main() {
|
|
|
1702
1760
|
const { runWorkspaceHealthCheck, formatWorkspaceHealthReport } =
|
|
1703
1761
|
await import("./config/config-doctor.mjs");
|
|
1704
1762
|
const configDirArg = getArgValue("--config-dir");
|
|
1705
|
-
const configDir = configDirArg || process.env.BOSUN_DIR ||
|
|
1763
|
+
const configDir = configDirArg || process.env.BOSUN_DIR || resolveConfigDirForCli();
|
|
1706
1764
|
const result = runWorkspaceHealthCheck({ configDir });
|
|
1707
1765
|
console.log(formatWorkspaceHealthReport(result));
|
|
1708
1766
|
process.exit(result.ok ? 0 : 1);
|
package/config/config.mjs
CHANGED
|
@@ -1292,6 +1292,13 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1292
1292
|
const normalizedRepoRootOverride = repoRootOverride
|
|
1293
1293
|
? resolve(repoRootOverride)
|
|
1294
1294
|
: "";
|
|
1295
|
+
const explicitConfigDirRaw =
|
|
1296
|
+
cli["config-dir"] || process.env.BOSUN_HOME || process.env.BOSUN_DIR || "";
|
|
1297
|
+
const hasExplicitConfigDir = String(explicitConfigDirRaw || "").trim() !== "";
|
|
1298
|
+
const allowRepoEnvWithExplicitConfig = isEnvEnabled(
|
|
1299
|
+
process.env.BOSUN_LOAD_REPO_ENV_WITH_EXPLICIT_CONFIG,
|
|
1300
|
+
false,
|
|
1301
|
+
);
|
|
1295
1302
|
let detectedRepoRoot = "";
|
|
1296
1303
|
const getFallbackRepoRoot = () => {
|
|
1297
1304
|
if (normalizedRepoRootOverride) return normalizedRepoRootOverride;
|
|
@@ -1301,8 +1308,7 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1301
1308
|
|
|
1302
1309
|
// Determine config directory (where bosun stores its config)
|
|
1303
1310
|
const configDir =
|
|
1304
|
-
|
|
1305
|
-
process.env.BOSUN_DIR ||
|
|
1311
|
+
explicitConfigDirRaw ||
|
|
1306
1312
|
resolveConfigDir(normalizedRepoRootOverride);
|
|
1307
1313
|
|
|
1308
1314
|
const configFile = loadConfigFile(configDir);
|
|
@@ -1345,16 +1351,20 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1345
1351
|
repositories[0] ||
|
|
1346
1352
|
null;
|
|
1347
1353
|
|
|
1348
|
-
// Resolve
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1354
|
+
// Resolve repo root. Explicit repo-root/REPO_ROOT must win over workspace clones
|
|
1355
|
+
// so source-based runs can pin execution to the developer working tree.
|
|
1356
|
+
const explicitRepoRoot = normalizedRepoRootOverride ||
|
|
1357
|
+
(process.env.REPO_ROOT ? resolve(process.env.REPO_ROOT) : "");
|
|
1351
1358
|
const selectedRepoPath = selectedRepository?.path || "";
|
|
1352
1359
|
const selectedRepoHasGit = selectedRepoPath && existsSync(resolve(selectedRepoPath, ".git"));
|
|
1353
1360
|
let repoRoot =
|
|
1354
|
-
|
|
1361
|
+
explicitRepoRoot ||
|
|
1362
|
+
(selectedRepoHasGit ? selectedRepoPath : null) ||
|
|
1363
|
+
getFallbackRepoRoot();
|
|
1355
1364
|
|
|
1356
|
-
// Resolve agent execution root
|
|
1357
|
-
|
|
1365
|
+
// Resolve agent execution root. Keep workspace-aware behavior by default,
|
|
1366
|
+
// but honor explicit repo-root/REPO_ROOT overrides.
|
|
1367
|
+
const agentRepoRoot = explicitRepoRoot || resolveAgentRepoRoot();
|
|
1358
1368
|
|
|
1359
1369
|
// Load .env from config dir — Bosun's .env is the primary source of truth
|
|
1360
1370
|
// for Bosun-specific configuration, so it should override any stale shell
|
|
@@ -1363,8 +1373,14 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1363
1373
|
const envOverride = reloadEnv || !isEnvEnabled(process.env.BOSUN_ENV_NO_OVERRIDE, false);
|
|
1364
1374
|
loadDotEnv(configDir, { override: envOverride });
|
|
1365
1375
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1376
|
+
const shouldLoadRepoEnv =
|
|
1377
|
+
resolve(repoRoot) !== resolve(configDir) &&
|
|
1378
|
+
(!hasExplicitConfigDir || allowRepoEnvWithExplicitConfig);
|
|
1379
|
+
|
|
1380
|
+
// Also load .env from repo root if different.
|
|
1381
|
+
// When config-dir/BOSUN_HOME is explicit, keep that environment isolated
|
|
1382
|
+
// from the repo root unless explicitly re-enabled.
|
|
1383
|
+
if (shouldLoadRepoEnv) {
|
|
1368
1384
|
loadDotEnv(repoRoot, { override: envOverride });
|
|
1369
1385
|
}
|
|
1370
1386
|
|
|
@@ -1419,14 +1435,17 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
1419
1435
|
repoRoot = (selHasGit ? selPath : null) || getFallbackRepoRoot();
|
|
1420
1436
|
}
|
|
1421
1437
|
|
|
1422
|
-
if (
|
|
1438
|
+
if (
|
|
1439
|
+
shouldLoadRepoEnv &&
|
|
1440
|
+
resolve(repoRoot) !== resolve(initialRepoRoot)
|
|
1441
|
+
) {
|
|
1423
1442
|
loadDotEnv(repoRoot, { override: envOverride });
|
|
1424
1443
|
}
|
|
1425
1444
|
|
|
1426
|
-
const envPaths = [
|
|
1427
|
-
|
|
1428
|
-
resolve(repoRoot, ".env")
|
|
1429
|
-
|
|
1445
|
+
const envPaths = [resolve(configDir, ".env")];
|
|
1446
|
+
if (shouldLoadRepoEnv) {
|
|
1447
|
+
envPaths.push(resolve(repoRoot, ".env"));
|
|
1448
|
+
}
|
|
1430
1449
|
const kanbanSource = resolveKanbanBackendSource({
|
|
1431
1450
|
envPaths,
|
|
1432
1451
|
configFilePath: configFile.path,
|
|
@@ -2396,3 +2415,4 @@ export {
|
|
|
2396
2415
|
resolveAgentRepoRoot,
|
|
2397
2416
|
};
|
|
2398
2417
|
export default loadConfig;
|
|
2418
|
+
|
package/desktop/package.json
CHANGED
package/infra/monitor.mjs
CHANGED
|
@@ -201,6 +201,7 @@ import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
|
201
201
|
// ── Task management subsystem imports ──────────────────────────────────────
|
|
202
202
|
import {
|
|
203
203
|
configureTaskStore,
|
|
204
|
+
canStartTask,
|
|
204
205
|
getTask as getInternalTask,
|
|
205
206
|
getTasksByStatus as getInternalTasksByStatus,
|
|
206
207
|
updateTask as updateInternalTask,
|
|
@@ -548,6 +549,9 @@ async function ensureWorkflowAutomationEngine() {
|
|
|
548
549
|
const services = {
|
|
549
550
|
telegram: telegramService,
|
|
550
551
|
kanban: kanbanService,
|
|
552
|
+
taskStore: {
|
|
553
|
+
canStartTask,
|
|
554
|
+
},
|
|
551
555
|
agentPool: agentPoolService,
|
|
552
556
|
meeting: meetingService,
|
|
553
557
|
prompts: Object.keys(promptServices).length > 0 ? promptServices : null,
|
|
@@ -1194,6 +1198,8 @@ const isMonitorTestRuntime =
|
|
|
1194
1198
|
.trim()
|
|
1195
1199
|
.toLowerCase(),
|
|
1196
1200
|
) || String(process.env.NODE_ENV || "").trim().toLowerCase() === "test";
|
|
1201
|
+
// Shared schedule poll hook used across startup/timer sections.
|
|
1202
|
+
let pollWorkflowSchedulesOnce = async () => {};
|
|
1197
1203
|
|
|
1198
1204
|
// ── Load unified configuration ──────────────────────────────────────────────
|
|
1199
1205
|
let config;
|
|
@@ -4901,9 +4907,13 @@ function getConfiguredKanbanProjectId(backend) {
|
|
|
4901
4907
|
);
|
|
4902
4908
|
}
|
|
4903
4909
|
|
|
4910
|
+
function hasUnresolvedTemplateToken(value) {
|
|
4911
|
+
return /{{[^{}]+}}/.test(String(value || ""));
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4904
4914
|
function resolveTaskIdForBackend(taskId, backend) {
|
|
4905
4915
|
const rawId = String(taskId || "").trim();
|
|
4906
|
-
if (!rawId) return null;
|
|
4916
|
+
if (!rawId || hasUnresolvedTemplateToken(rawId)) return null;
|
|
4907
4917
|
if (backend !== "github") return rawId;
|
|
4908
4918
|
const directMatch = parseGitHubIssueNumber(rawId);
|
|
4909
4919
|
if (directMatch) return directMatch;
|
|
@@ -13509,14 +13519,19 @@ safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
|
|
|
13509
13519
|
// Legacy periodic maintenance sweep removed (workflow-only control).
|
|
13510
13520
|
|
|
13511
13521
|
// ── Workflow schedule trigger polling ───────────────────────────────────────
|
|
13512
|
-
// Check all installed workflows
|
|
13513
|
-
//
|
|
13514
|
-
//
|
|
13522
|
+
// Check all installed polling workflows (trigger.schedule, trigger.scheduled_once,
|
|
13523
|
+
// trigger.task_available, trigger.task_low) and fire any whose interval has elapsed.
|
|
13524
|
+
// This keeps scheduled and task-poll lifecycle templates executing without hardcoded
|
|
13525
|
+
// per-workflow timers.
|
|
13515
13526
|
const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
|
|
13516
|
-
|
|
13527
|
+
pollWorkflowSchedulesOnce = async function pollWorkflowSchedulesOnce(
|
|
13528
|
+
triggerSource = "schedule-poll",
|
|
13529
|
+
opts = {},
|
|
13530
|
+
) {
|
|
13517
13531
|
try {
|
|
13518
13532
|
const engine = await ensureWorkflowAutomationEngine();
|
|
13519
13533
|
if (!engine?.evaluateScheduleTriggers) return;
|
|
13534
|
+
const includeTaskPoll = opts?.includeTaskPoll !== false;
|
|
13520
13535
|
|
|
13521
13536
|
const triggered = engine.evaluateScheduleTriggers();
|
|
13522
13537
|
if (!Array.isArray(triggered) || triggered.length === 0) return;
|
|
@@ -13524,9 +13539,18 @@ safeSetInterval("workflow-schedule-check", async () => {
|
|
|
13524
13539
|
for (const match of triggered) {
|
|
13525
13540
|
const workflowId = String(match?.workflowId || "").trim();
|
|
13526
13541
|
if (!workflowId) continue;
|
|
13542
|
+
if (!includeTaskPoll) {
|
|
13543
|
+
const workflow = typeof engine.get === "function" ? engine.get(workflowId) : null;
|
|
13544
|
+
const triggerNode = Array.isArray(workflow?.nodes)
|
|
13545
|
+
? workflow.nodes.find((node) => node?.id === match?.triggeredBy)
|
|
13546
|
+
: null;
|
|
13547
|
+
if (triggerNode?.type === "trigger.task_available" || triggerNode?.type === "trigger.task_low") {
|
|
13548
|
+
continue;
|
|
13549
|
+
}
|
|
13550
|
+
}
|
|
13527
13551
|
void engine
|
|
13528
13552
|
.execute(workflowId, {
|
|
13529
|
-
_triggerSource:
|
|
13553
|
+
_triggerSource: triggerSource,
|
|
13530
13554
|
_triggeredBy: match?.triggeredBy || null,
|
|
13531
13555
|
_lastRunAt: Date.now(),
|
|
13532
13556
|
repoRoot,
|
|
@@ -13554,9 +13578,12 @@ safeSetInterval("workflow-schedule-check", async () => {
|
|
|
13554
13578
|
);
|
|
13555
13579
|
}
|
|
13556
13580
|
} catch (err) {
|
|
13557
|
-
// Schedule evaluation must not crash the monitor
|
|
13558
13581
|
console.warn(`[workflows] schedule-check error: ${err?.message || err}`);
|
|
13559
13582
|
}
|
|
13583
|
+
};
|
|
13584
|
+
|
|
13585
|
+
safeSetInterval("workflow-schedule-check", async () => {
|
|
13586
|
+
await pollWorkflowSchedulesOnce();
|
|
13560
13587
|
}, scheduleCheckIntervalMs);
|
|
13561
13588
|
|
|
13562
13589
|
// Legacy merged PR check removed (workflow-only control).
|
|
@@ -13653,12 +13680,15 @@ if (telegramWeeklyReportEnabled) {
|
|
|
13653
13680
|
}
|
|
13654
13681
|
|
|
13655
13682
|
// ── Self-updating: poll npm every 10 min, auto-install + restart ────────────
|
|
13683
|
+
const isDaemonChildForAutoUpdate =
|
|
13684
|
+
process.argv.includes("--daemon-child") || process.env.BOSUN_DAEMON === "1";
|
|
13656
13685
|
startAutoUpdateLoop({
|
|
13657
13686
|
onRestart: (reason) => restartSelf(reason),
|
|
13658
13687
|
onNotify: (msg) =>
|
|
13659
13688
|
// Priority 1 (critical) bypasses the live digest so the user gets a
|
|
13660
13689
|
// direct push notification for update-detected and restarting events.
|
|
13661
13690
|
sendTelegramMessage(msg, { priority: 1, skipDedup: true }).catch(() => {}),
|
|
13691
|
+
trackParent: !isDaemonChildForAutoUpdate,
|
|
13662
13692
|
});
|
|
13663
13693
|
|
|
13664
13694
|
startWatcher();
|
|
@@ -13822,13 +13852,16 @@ let errorDetector = null;
|
|
|
13822
13852
|
let agentSupervisor = null;
|
|
13823
13853
|
|
|
13824
13854
|
if (!isMonitorTestRuntime) {
|
|
13825
|
-
if (workflowAutomationEnabled) {
|
|
13826
|
-
|
|
13827
|
-
}
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13855
|
+
if (workflowAutomationEnabled) {
|
|
13856
|
+
await ensureWorkflowAutomationEngine().catch(() => {});
|
|
13857
|
+
void pollWorkflowSchedulesOnce("startup", { includeTaskPoll: false }).catch((err) => {
|
|
13858
|
+
console.warn(`[workflows] startup poll error: ${err?.message || err}`);
|
|
13859
|
+
});
|
|
13860
|
+
} else {
|
|
13861
|
+
console.log(
|
|
13862
|
+
"[workflows] automation disabled (set WORKFLOW_AUTOMATION_ENABLED=true to enable event-driven workflow triggers)",
|
|
13863
|
+
);
|
|
13864
|
+
}
|
|
13832
13865
|
// ── Task Management Subsystem Initialization ────────────────────────────────
|
|
13833
13866
|
try {
|
|
13834
13867
|
mkdirSync(monitorStateCacheDir, { recursive: true });
|
|
@@ -14065,6 +14098,11 @@ if (isExecutorDisabled()) {
|
|
|
14065
14098
|
};
|
|
14066
14099
|
internalTaskExecutor = getTaskExecutor(execOpts);
|
|
14067
14100
|
internalTaskExecutor.start();
|
|
14101
|
+
if (workflowOwnsTaskExecutorLifecycle) {
|
|
14102
|
+
void pollWorkflowSchedulesOnce("startup").catch((err) => {
|
|
14103
|
+
console.warn(`[workflows] startup poll error: ${err?.message || err}`);
|
|
14104
|
+
});
|
|
14105
|
+
}
|
|
14068
14106
|
|
|
14069
14107
|
// Write executor slots to status file every 30s for Telegram /tasks
|
|
14070
14108
|
startStatusFileWriter(30000);
|
|
@@ -14629,5 +14667,3 @@ export {
|
|
|
14629
14667
|
// Workflow event bridge — for fleet/kanban modules to emit events
|
|
14630
14668
|
queueWorkflowEvent,
|
|
14631
14669
|
};
|
|
14632
|
-
|
|
14633
|
-
|
|
@@ -551,6 +551,7 @@ export class SessionTracker {
|
|
|
551
551
|
status: s.status,
|
|
552
552
|
workspaceId: String(s?.metadata?.workspaceId || "").trim() || null,
|
|
553
553
|
workspaceDir: String(s?.metadata?.workspaceDir || "").trim() || null,
|
|
554
|
+
branch: String(s?.metadata?.branch || "").trim() || null,
|
|
554
555
|
turnCount: s.turnCount || 0,
|
|
555
556
|
createdAt: s.createdAt || new Date(s.startedAt).toISOString(),
|
|
556
557
|
lastActiveAt: s.lastActiveAt || new Date(s.lastActivityAt).toISOString(),
|
package/infra/sync-engine.mjs
CHANGED
|
@@ -1018,8 +1018,13 @@ export class SyncEngine {
|
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
1020
|
async #createExternalTask(task, baseBranchCandidate = null) {
|
|
1021
|
+
const normalizedTitle = String(task?.title || "").trim();
|
|
1022
|
+
if (!normalizedTitle) {
|
|
1023
|
+
throw new Error(`cannot create external task for ${task?.id || "<unknown>"}: missing title`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1021
1026
|
const payload = {
|
|
1022
|
-
title:
|
|
1027
|
+
title: normalizedTitle,
|
|
1023
1028
|
description: task.description || "",
|
|
1024
1029
|
status: "todo",
|
|
1025
1030
|
assignee: task.assignee || null,
|
package/infra/update-check.mjs
CHANGED
|
@@ -463,9 +463,7 @@ export async function forceUpdate(currentVersion) {
|
|
|
463
463
|
stdio: "inherit",
|
|
464
464
|
timeout: 120000,
|
|
465
465
|
});
|
|
466
|
-
console.log(
|
|
467
|
-
`\n :check: Updated to v${latest}. Restart bosun to use the new version.\n`,
|
|
468
|
-
);
|
|
466
|
+
console.log(`\n :check: Updated to v${latest}.\n`);
|
|
469
467
|
return true; // update was installed
|
|
470
468
|
} catch (err) {
|
|
471
469
|
console.error(`\n :close: Update failed: ${err.message}`);
|
|
@@ -556,6 +554,7 @@ function isSuppressedStreamNoiseError(err) {
|
|
|
556
554
|
* @param {function} [opts.onNotify] - Called with message string for Telegram/log
|
|
557
555
|
* @param {number} [opts.intervalMs] - Poll interval (default: 10 min)
|
|
558
556
|
* @param {number} [opts.parentPid] - Parent process PID to monitor (default: process.ppid)
|
|
557
|
+
* @param {boolean} [opts.trackParent] - Monitor parent liveness (default: true)
|
|
559
558
|
*/
|
|
560
559
|
export function startAutoUpdateLoop(opts = {}) {
|
|
561
560
|
if (process.env.BOSUN_SKIP_AUTO_UPDATE === "1") {
|
|
@@ -596,14 +595,21 @@ export function startAutoUpdateLoop(opts = {}) {
|
|
|
596
595
|
|
|
597
596
|
// Register cleanup handlers to prevent zombie processes
|
|
598
597
|
registerCleanupHandlers();
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
598
|
+
const trackParent = opts.trackParent !== false;
|
|
599
|
+
|
|
600
|
+
// Track parent process when enabled. Detached daemon-child monitor sessions
|
|
601
|
+
// should opt out so they do not self-terminate when the launcher exits.
|
|
602
|
+
if (trackParent) {
|
|
603
|
+
if (opts.parentPid) {
|
|
604
|
+
parentPid = opts.parentPid;
|
|
605
|
+
console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
|
|
606
|
+
} else {
|
|
607
|
+
parentPid = process.ppid; // Track parent by default
|
|
608
|
+
console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
|
|
609
|
+
}
|
|
604
610
|
} else {
|
|
605
|
-
parentPid =
|
|
606
|
-
console.log(
|
|
611
|
+
parentPid = null;
|
|
612
|
+
console.log("[auto-update] Parent process monitoring disabled");
|
|
607
613
|
}
|
|
608
614
|
|
|
609
615
|
console.log(
|