bosun 0.40.3 → 0.40.5
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 +11 -0
- package/agent/review-agent.mjs +48 -19
- package/cli.mjs +41 -2
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +44 -13
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +15 -7
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +2 -1
- package/server/ui-server.mjs +385 -39
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/modules/mui.js +600 -397
- 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 +291 -70
- package/workflow/workflow-engine.mjs +0 -24
- package/workflow/workflow-nodes.mjs +219 -20
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +11 -0
- package/workspace/worktree-manager.mjs +6 -0
package/README.md
CHANGED
|
@@ -181,6 +181,17 @@ 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
|
+
## Star History
|
|
187
|
+
|
|
188
|
+
<a href="https://www.star-history.com/?repos=VirtEngine%2FBosun&type=date&legend=top-left">
|
|
189
|
+
<picture>
|
|
190
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&theme=dark&legend=top-left" />
|
|
191
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
|
|
192
|
+
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
|
|
193
|
+
</picture>
|
|
194
|
+
</a>
|
|
184
195
|
---
|
|
185
196
|
|
|
186
197
|
## License
|
package/agent/review-agent.mjs
CHANGED
|
@@ -122,24 +122,36 @@ function extractRepoSlug(prUrl) {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Get the PR diff using `gh pr diff` or `git diff`.
|
|
125
|
-
* @param {{ prUrl?: string, branchName?: string }} opts
|
|
125
|
+
* @param {{ prUrl?: string, prNumber?: number|string, repoSlug?: string, branchName?: string, cwd?: string|null }} opts
|
|
126
126
|
* @returns {{ diff: string, truncated: boolean }}
|
|
127
127
|
*/
|
|
128
|
-
function getPrDiff({ prUrl, branchName }) {
|
|
128
|
+
function getPrDiff({ prUrl, prNumber, repoSlug, branchName, cwd }) {
|
|
129
129
|
let diff = "";
|
|
130
|
+
const commandOptions = {
|
|
131
|
+
encoding: "utf8",
|
|
132
|
+
timeout: 30_000,
|
|
133
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
134
|
+
...(cwd ? { cwd } : {}),
|
|
135
|
+
};
|
|
130
136
|
|
|
131
137
|
// Strategy 1: gh pr diff
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
const resolvedPrNumber = Number.isFinite(Number(prNumber))
|
|
139
|
+
? Number(prNumber)
|
|
140
|
+
: extractPrNumber(prUrl);
|
|
141
|
+
const resolvedRepoSlug = String(repoSlug || "").trim() || extractRepoSlug(prUrl);
|
|
142
|
+
if (resolvedPrNumber) {
|
|
135
143
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
["pr", "diff", String(
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
const attempts = [];
|
|
145
|
+
if (resolvedRepoSlug) {
|
|
146
|
+
attempts.push(["pr", "diff", String(resolvedPrNumber), "--repo", resolvedRepoSlug]);
|
|
147
|
+
}
|
|
148
|
+
attempts.push(["pr", "diff", String(resolvedPrNumber)]);
|
|
149
|
+
for (const args of attempts) {
|
|
150
|
+
const result = spawnSync("gh", args, commandOptions);
|
|
151
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
152
|
+
diff = result.stdout;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
143
155
|
}
|
|
144
156
|
} catch {
|
|
145
157
|
/* fall through to git diff */
|
|
@@ -149,13 +161,16 @@ function getPrDiff({ prUrl, branchName }) {
|
|
|
149
161
|
// Strategy 2: git diff main...<branch>
|
|
150
162
|
if (!diff && branchName) {
|
|
151
163
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
const attempts = [
|
|
165
|
+
["diff", `origin/main...${branchName}`],
|
|
166
|
+
["diff", `main...${branchName}`],
|
|
167
|
+
];
|
|
168
|
+
for (const args of attempts) {
|
|
169
|
+
const result = spawnSync("git", args, commandOptions);
|
|
170
|
+
if (result.status === 0 && result.stdout?.trim()) {
|
|
171
|
+
diff = result.stdout;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
159
174
|
}
|
|
160
175
|
} catch {
|
|
161
176
|
/* ignore */
|
|
@@ -343,6 +358,17 @@ export class ReviewAgent {
|
|
|
343
358
|
console.warn(`${TAG} queueReview called without task id — skipping`);
|
|
344
359
|
return;
|
|
345
360
|
}
|
|
361
|
+
const hasReviewReference = Boolean(
|
|
362
|
+
String(task.prUrl || "").trim() ||
|
|
363
|
+
String(task.prNumber || "").trim() ||
|
|
364
|
+
String(task.branchName || "").trim(),
|
|
365
|
+
);
|
|
366
|
+
if (!hasReviewReference) {
|
|
367
|
+
console.warn(
|
|
368
|
+
`${TAG} queueReview skipped for ${task.id}: no prUrl, prNumber, or branchName`,
|
|
369
|
+
);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
346
372
|
|
|
347
373
|
if (this.#seen.has(task.id)) {
|
|
348
374
|
console.log(`${TAG} task ${task.id} already queued/in-flight — skipping`);
|
|
@@ -463,7 +489,10 @@ export class ReviewAgent {
|
|
|
463
489
|
// 1. Get PR diff
|
|
464
490
|
const { diff, truncated } = getPrDiff({
|
|
465
491
|
prUrl: task.prUrl,
|
|
492
|
+
prNumber: task.prNumber,
|
|
493
|
+
repoSlug: task.repoSlug,
|
|
466
494
|
branchName: task.branchName,
|
|
495
|
+
cwd: task.worktreePath || null,
|
|
467
496
|
});
|
|
468
497
|
|
|
469
498
|
if (!diff) {
|
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
|
{
|
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
|
@@ -1198,6 +1198,8 @@ const isMonitorTestRuntime =
|
|
|
1198
1198
|
.trim()
|
|
1199
1199
|
.toLowerCase(),
|
|
1200
1200
|
) || String(process.env.NODE_ENV || "").trim().toLowerCase() === "test";
|
|
1201
|
+
// Shared schedule poll hook used across startup/timer sections.
|
|
1202
|
+
let pollWorkflowSchedulesOnce = async () => {};
|
|
1201
1203
|
|
|
1202
1204
|
// ── Load unified configuration ──────────────────────────────────────────────
|
|
1203
1205
|
let config;
|
|
@@ -4905,9 +4907,13 @@ function getConfiguredKanbanProjectId(backend) {
|
|
|
4905
4907
|
);
|
|
4906
4908
|
}
|
|
4907
4909
|
|
|
4910
|
+
function hasUnresolvedTemplateToken(value) {
|
|
4911
|
+
return /{{[^{}]+}}/.test(String(value || ""));
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4908
4914
|
function resolveTaskIdForBackend(taskId, backend) {
|
|
4909
4915
|
const rawId = String(taskId || "").trim();
|
|
4910
|
-
if (!rawId) return null;
|
|
4916
|
+
if (!rawId || hasUnresolvedTemplateToken(rawId)) return null;
|
|
4911
4917
|
if (backend !== "github") return rawId;
|
|
4912
4918
|
const directMatch = parseGitHubIssueNumber(rawId);
|
|
4913
4919
|
if (directMatch) return directMatch;
|
|
@@ -13518,10 +13524,14 @@ safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
|
|
|
13518
13524
|
// This keeps scheduled and task-poll lifecycle templates executing without hardcoded
|
|
13519
13525
|
// per-workflow timers.
|
|
13520
13526
|
const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
|
|
13521
|
-
|
|
13527
|
+
pollWorkflowSchedulesOnce = async function pollWorkflowSchedulesOnce(
|
|
13528
|
+
triggerSource = "schedule-poll",
|
|
13529
|
+
opts = {},
|
|
13530
|
+
) {
|
|
13522
13531
|
try {
|
|
13523
13532
|
const engine = await ensureWorkflowAutomationEngine();
|
|
13524
13533
|
if (!engine?.evaluateScheduleTriggers) return;
|
|
13534
|
+
const includeTaskPoll = opts?.includeTaskPoll !== false;
|
|
13525
13535
|
|
|
13526
13536
|
const triggered = engine.evaluateScheduleTriggers();
|
|
13527
13537
|
if (!Array.isArray(triggered) || triggered.length === 0) return;
|
|
@@ -13529,9 +13539,18 @@ safeSetInterval("workflow-schedule-check", async () => {
|
|
|
13529
13539
|
for (const match of triggered) {
|
|
13530
13540
|
const workflowId = String(match?.workflowId || "").trim();
|
|
13531
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
|
+
}
|
|
13532
13551
|
void engine
|
|
13533
13552
|
.execute(workflowId, {
|
|
13534
|
-
_triggerSource:
|
|
13553
|
+
_triggerSource: triggerSource,
|
|
13535
13554
|
_triggeredBy: match?.triggeredBy || null,
|
|
13536
13555
|
_lastRunAt: Date.now(),
|
|
13537
13556
|
repoRoot,
|
|
@@ -13559,9 +13578,12 @@ safeSetInterval("workflow-schedule-check", async () => {
|
|
|
13559
13578
|
);
|
|
13560
13579
|
}
|
|
13561
13580
|
} catch (err) {
|
|
13562
|
-
// Schedule evaluation must not crash the monitor
|
|
13563
13581
|
console.warn(`[workflows] schedule-check error: ${err?.message || err}`);
|
|
13564
13582
|
}
|
|
13583
|
+
};
|
|
13584
|
+
|
|
13585
|
+
safeSetInterval("workflow-schedule-check", async () => {
|
|
13586
|
+
await pollWorkflowSchedulesOnce();
|
|
13565
13587
|
}, scheduleCheckIntervalMs);
|
|
13566
13588
|
|
|
13567
13589
|
// Legacy merged PR check removed (workflow-only control).
|
|
@@ -13658,12 +13680,15 @@ if (telegramWeeklyReportEnabled) {
|
|
|
13658
13680
|
}
|
|
13659
13681
|
|
|
13660
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";
|
|
13661
13685
|
startAutoUpdateLoop({
|
|
13662
13686
|
onRestart: (reason) => restartSelf(reason),
|
|
13663
13687
|
onNotify: (msg) =>
|
|
13664
13688
|
// Priority 1 (critical) bypasses the live digest so the user gets a
|
|
13665
13689
|
// direct push notification for update-detected and restarting events.
|
|
13666
13690
|
sendTelegramMessage(msg, { priority: 1, skipDedup: true }).catch(() => {}),
|
|
13691
|
+
trackParent: !isDaemonChildForAutoUpdate,
|
|
13667
13692
|
});
|
|
13668
13693
|
|
|
13669
13694
|
startWatcher();
|
|
@@ -13827,13 +13852,16 @@ let errorDetector = null;
|
|
|
13827
13852
|
let agentSupervisor = null;
|
|
13828
13853
|
|
|
13829
13854
|
if (!isMonitorTestRuntime) {
|
|
13830
|
-
if (workflowAutomationEnabled) {
|
|
13831
|
-
|
|
13832
|
-
}
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
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
|
+
}
|
|
13837
13865
|
// ── Task Management Subsystem Initialization ────────────────────────────────
|
|
13838
13866
|
try {
|
|
13839
13867
|
mkdirSync(monitorStateCacheDir, { recursive: true });
|
|
@@ -14070,6 +14098,11 @@ if (isExecutorDisabled()) {
|
|
|
14070
14098
|
};
|
|
14071
14099
|
internalTaskExecutor = getTaskExecutor(execOpts);
|
|
14072
14100
|
internalTaskExecutor.start();
|
|
14101
|
+
if (workflowOwnsTaskExecutorLifecycle) {
|
|
14102
|
+
void pollWorkflowSchedulesOnce("startup").catch((err) => {
|
|
14103
|
+
console.warn(`[workflows] startup poll error: ${err?.message || err}`);
|
|
14104
|
+
});
|
|
14105
|
+
}
|
|
14073
14106
|
|
|
14074
14107
|
// Write executor slots to status file every 30s for Telegram /tasks
|
|
14075
14108
|
startStatusFileWriter(30000);
|
|
@@ -14634,5 +14667,3 @@ export {
|
|
|
14634
14667
|
// Workflow event bridge — for fleet/kanban modules to emit events
|
|
14635
14668
|
queueWorkflowEvent,
|
|
14636
14669
|
};
|
|
14637
|
-
|
|
14638
|
-
|
|
@@ -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
|
@@ -554,6 +554,7 @@ function isSuppressedStreamNoiseError(err) {
|
|
|
554
554
|
* @param {function} [opts.onNotify] - Called with message string for Telegram/log
|
|
555
555
|
* @param {number} [opts.intervalMs] - Poll interval (default: 10 min)
|
|
556
556
|
* @param {number} [opts.parentPid] - Parent process PID to monitor (default: process.ppid)
|
|
557
|
+
* @param {boolean} [opts.trackParent] - Monitor parent liveness (default: true)
|
|
557
558
|
*/
|
|
558
559
|
export function startAutoUpdateLoop(opts = {}) {
|
|
559
560
|
if (process.env.BOSUN_SKIP_AUTO_UPDATE === "1") {
|
|
@@ -594,14 +595,21 @@ export function startAutoUpdateLoop(opts = {}) {
|
|
|
594
595
|
|
|
595
596
|
// Register cleanup handlers to prevent zombie processes
|
|
596
597
|
registerCleanupHandlers();
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
}
|
|
602
610
|
} else {
|
|
603
|
-
parentPid =
|
|
604
|
-
console.log(
|
|
611
|
+
parentPid = null;
|
|
612
|
+
console.log("[auto-update] Parent process monitoring disabled");
|
|
605
613
|
}
|
|
606
614
|
|
|
607
615
|
console.log(
|
|
@@ -792,11 +792,18 @@ class InternalAdapter {
|
|
|
792
792
|
taskUrl: task.taskUrl || null,
|
|
793
793
|
createdAt: task.createdAt || null,
|
|
794
794
|
updatedAt: task.updatedAt || null,
|
|
795
|
+
lastActivityAt: task.lastActivityAt || task.updatedAt || null,
|
|
796
|
+
timeline: Array.isArray(task.timeline) ? task.timeline : [],
|
|
797
|
+
workflowRuns: Array.isArray(task.workflowRuns) ? task.workflowRuns : [],
|
|
798
|
+
statusHistory: Array.isArray(task.statusHistory) ? task.statusHistory : [],
|
|
795
799
|
backend: "internal",
|
|
796
800
|
attachments: mergedAttachments,
|
|
797
801
|
comments: normalizedComments,
|
|
798
802
|
meta: {
|
|
799
803
|
...(task.meta || {}),
|
|
804
|
+
timeline: Array.isArray(task.timeline) ? task.timeline : (Array.isArray(task.meta?.timeline) ? task.meta.timeline : []),
|
|
805
|
+
workflowRuns: Array.isArray(task.workflowRuns) ? task.workflowRuns : (Array.isArray(task.meta?.workflowRuns) ? task.meta.workflowRuns : []),
|
|
806
|
+
statusHistory: Array.isArray(task.statusHistory) ? task.statusHistory : (Array.isArray(task.meta?.statusHistory) ? task.meta.statusHistory : []),
|
|
800
807
|
comments: normalizedComments,
|
|
801
808
|
attachments: mergedAttachments,
|
|
802
809
|
},
|
|
@@ -3501,6 +3508,10 @@ class GitHubIssuesAdapter {
|
|
|
3501
3508
|
}
|
|
3502
3509
|
|
|
3503
3510
|
async addProjectV2DraftIssue(projectNumber, title, body = "") {
|
|
3511
|
+
const normalizedTitle = String(title || "").trim();
|
|
3512
|
+
if (!normalizedTitle) {
|
|
3513
|
+
throw new Error("[kanban] github createTask requires non-empty title");
|
|
3514
|
+
}
|
|
3504
3515
|
const projectId = await this.getProjectNodeId(projectNumber);
|
|
3505
3516
|
if (!projectId) return null;
|
|
3506
3517
|
const mutation = `
|
|
@@ -3508,7 +3519,7 @@ class GitHubIssuesAdapter {
|
|
|
3508
3519
|
addProjectV2DraftIssue(
|
|
3509
3520
|
input: {
|
|
3510
3521
|
projectId: ${this._escapeGraphQLString(projectId)},
|
|
3511
|
-
title: ${this._escapeGraphQLString(
|
|
3522
|
+
title: ${this._escapeGraphQLString(normalizedTitle)},
|
|
3512
3523
|
body: ${this._escapeGraphQLString(body)}
|
|
3513
3524
|
}
|
|
3514
3525
|
) {
|
|
@@ -3550,6 +3561,10 @@ class GitHubIssuesAdapter {
|
|
|
3550
3561
|
}
|
|
3551
3562
|
|
|
3552
3563
|
async createTask(_projectId, taskData) {
|
|
3564
|
+
const normalizedTitle = String(taskData?.title || "").trim();
|
|
3565
|
+
if (!normalizedTitle) {
|
|
3566
|
+
throw new Error("[kanban] github createTask requires non-empty title");
|
|
3567
|
+
}
|
|
3553
3568
|
const wantsDraftCreate = Boolean(taskData?.draft || taskData?.createDraft);
|
|
3554
3569
|
const shouldConvertDraft = Boolean(
|
|
3555
3570
|
taskData?.convertDraft || taskData?.convertToIssue,
|
|
@@ -3563,7 +3578,7 @@ class GitHubIssuesAdapter {
|
|
|
3563
3578
|
if (wantsDraftCreate && projectNumber) {
|
|
3564
3579
|
const draftItemId = await this.addProjectV2DraftIssue(
|
|
3565
3580
|
projectNumber,
|
|
3566
|
-
|
|
3581
|
+
normalizedTitle,
|
|
3567
3582
|
taskData.description || "",
|
|
3568
3583
|
);
|
|
3569
3584
|
if (!draftItemId) {
|
|
@@ -3572,7 +3587,7 @@ class GitHubIssuesAdapter {
|
|
|
3572
3587
|
if (!shouldConvertDraft) {
|
|
3573
3588
|
return {
|
|
3574
3589
|
id: `draft:${draftItemId}`,
|
|
3575
|
-
title:
|
|
3590
|
+
title: normalizedTitle,
|
|
3576
3591
|
description: taskData.description || "",
|
|
3577
3592
|
status: requestedStatus,
|
|
3578
3593
|
assignee: null,
|
|
@@ -3665,7 +3680,7 @@ class GitHubIssuesAdapter {
|
|
|
3665
3680
|
"--repo",
|
|
3666
3681
|
`${this._owner}/${this._repo}`,
|
|
3667
3682
|
"--title",
|
|
3668
|
-
|
|
3683
|
+
normalizedTitle,
|
|
3669
3684
|
"--body",
|
|
3670
3685
|
taskData.description || "",
|
|
3671
3686
|
];
|
|
@@ -640,6 +640,30 @@ function Get-OrchestratorMutexName {
|
|
|
640
640
|
return "$BaseName.$suffix"
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
function Repair-MainRepoGitConfigCorruption {
|
|
644
|
+
$repoRoot = $null
|
|
645
|
+
try {
|
|
646
|
+
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
if (-not $repoRoot) {
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
$bareValue = git -C $repoRoot config --bool --get core.bare 2>$null
|
|
656
|
+
if ($LASTEXITCODE -eq 0 -and "$bareValue".Trim().ToLowerInvariant() -eq "true") {
|
|
657
|
+
Write-Log "Detected core.bare=true on main repo; repairing git config corruption" -Level "WARN"
|
|
658
|
+
git -C $repoRoot config --local core.bare false 2>&1 | Out-Null
|
|
659
|
+
git -C $repoRoot config --local --unset-all core.worktree 2>&1 | Out-Null
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
Write-Log "Failed to repair main repo git config corruption: $($_.Exception.Message)" -Level "WARN"
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
643
667
|
function Enter-OrchestratorMutex {
|
|
644
668
|
param([string]$Name = (Get-OrchestratorMutexName))
|
|
645
669
|
$createdNew = $false
|
|
@@ -3274,6 +3298,7 @@ function Invoke-DirectRebaseIsolatedWorktree {
|
|
|
3274
3298
|
throw "git worktree add failed: $addOut"
|
|
3275
3299
|
}
|
|
3276
3300
|
$addedWorktree = $true
|
|
3301
|
+
Repair-MainRepoGitConfigCorruption
|
|
3277
3302
|
|
|
3278
3303
|
# Hard-clean the isolated worktree to remove any stale filesystem residue.
|
|
3279
3304
|
git -C $tempWorktreePath reset --hard HEAD 2>&1 | Out-Null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.5",
|
|
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",
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
"bench:swebench:import": "node bench/swebench/bosun-swebench.mjs import",
|
|
130
130
|
"bench:swebench:export": "node bench/swebench/bosun-swebench.mjs export",
|
|
131
131
|
"bench:swebench:eval": "node bench/swebench/bosun-swebench.mjs eval",
|
|
132
|
+
"bench:library:resolver": "node bench/library/library-resolver-bench.mjs",
|
|
132
133
|
"mutate": "npx stryker run",
|
|
133
134
|
"mutate:incremental": "npx stryker run --incremental",
|
|
134
135
|
"mutate:report": "node scripts/mutation-report.mjs",
|