bosun 0.40.2 → 0.40.3
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 +86 -67
- package/infra/monitor.mjs +8 -3
- package/infra/update-check.mjs +1 -3
- package/package.json +1 -1
- package/server/ui-server.mjs +117 -0
- package/task/task-executor.mjs +690 -6
- package/task/task-store.mjs +116 -1
- package/ui/demo.html +26 -1
- package/ui/styles/components.css +43 -3
- package/ui/tabs/tasks.js +387 -97
- package/workflow/workflow-engine.mjs +30 -5
- package/workflow/workflow-nodes.mjs +102 -2
- package/workspace/workspace-manager.mjs +14 -0
- package/workspace/worktree-manager.mjs +2 -2
|
@@ -1282,12 +1282,12 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1282
1282
|
// ── Schedule trigger evaluation ──────────────────────────────────────────
|
|
1283
1283
|
|
|
1284
1284
|
/**
|
|
1285
|
-
* Evaluate
|
|
1285
|
+
* Evaluate polling workflows.
|
|
1286
1286
|
* Unlike evaluateTriggers() (event-driven), this is polling-based and should
|
|
1287
1287
|
* be called periodically (e.g. every 60s) by the monitor.
|
|
1288
1288
|
*
|
|
1289
1289
|
* Returns an array of { workflowId, triggeredBy } for workflows whose
|
|
1290
|
-
*
|
|
1290
|
+
* polling interval has elapsed since their last completed run.
|
|
1291
1291
|
*/
|
|
1292
1292
|
evaluateScheduleTriggers() {
|
|
1293
1293
|
if (!this._loaded) this.load();
|
|
@@ -1304,12 +1304,22 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1304
1304
|
);
|
|
1305
1305
|
if (alreadyRunning) continue;
|
|
1306
1306
|
|
|
1307
|
-
const triggerNodes = (def.nodes || []).filter(
|
|
1308
|
-
|
|
1307
|
+
const triggerNodes = (def.nodes || []).filter((n) =>
|
|
1308
|
+
n.type === "trigger.schedule"
|
|
1309
|
+
|| n.type === "trigger.scheduled_once"
|
|
1310
|
+
|| n.type === "trigger.task_available"
|
|
1311
|
+
|| n.type === "trigger.task_low",
|
|
1309
1312
|
);
|
|
1310
1313
|
|
|
1311
1314
|
for (const tNode of triggerNodes) {
|
|
1312
|
-
|
|
1315
|
+
let intervalMs = 3600000;
|
|
1316
|
+
if (tNode.type === "trigger.task_available") {
|
|
1317
|
+
intervalMs = Number(tNode.config?.pollIntervalMs) || 30000;
|
|
1318
|
+
} else if (tNode.type === "trigger.task_low") {
|
|
1319
|
+
intervalMs = Number(tNode.config?.pollIntervalMs) || 60000;
|
|
1320
|
+
} else {
|
|
1321
|
+
intervalMs = Number(tNode.config?.intervalMs) || 3600000;
|
|
1322
|
+
}
|
|
1313
1323
|
|
|
1314
1324
|
// Find the most recent completed run for this workflow
|
|
1315
1325
|
let lastRunAt = 0;
|
|
@@ -1781,6 +1791,7 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1781
1791
|
const node = nodeMap.get(nodeId);
|
|
1782
1792
|
const edges = adjacency.get(nodeId) || [];
|
|
1783
1793
|
const sourceOutput = ctx.getNodeOutput(nodeId);
|
|
1794
|
+
const triggerBlocked = node?.type?.startsWith("trigger.") && sourceOutput?.triggered === false;
|
|
1784
1795
|
const selectedPortRaw =
|
|
1785
1796
|
sourceOutput?.matchedPort ??
|
|
1786
1797
|
sourceOutput?.port ??
|
|
@@ -1790,6 +1801,19 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
1790
1801
|
? selectedPortRaw.trim()
|
|
1791
1802
|
: null;
|
|
1792
1803
|
|
|
1804
|
+
if (triggerBlocked) {
|
|
1805
|
+
for (const edge of edges) {
|
|
1806
|
+
if (edge.backEdge) continue;
|
|
1807
|
+
const newDegree = (inDegree.get(edge.target) || 1) - 1;
|
|
1808
|
+
inDegree.set(edge.target, newDegree);
|
|
1809
|
+
if (newDegree <= 0 && !executed.has(edge.target)) {
|
|
1810
|
+
ctx.setNodeStatus(edge.target, NodeStatus.SKIPPED);
|
|
1811
|
+
executed.add(edge.target);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1793
1817
|
// Handle loop.for_each: iterate downstream subgraph per item
|
|
1794
1818
|
if (node?.type === "loop.for_each" && ctx.getNodeStatus(nodeId) === NodeStatus.COMPLETED) {
|
|
1795
1819
|
const loopOutput = ctx.getNodeOutput(nodeId);
|
|
@@ -2630,3 +2654,4 @@ export async function executeWorkflow(id, data, opts) { return getWorkflowEngine
|
|
|
2630
2654
|
export async function retryWorkflowRun(runId, retryOpts, engineOpts) { return getWorkflowEngine(engineOpts).retryRun(runId, retryOpts); }
|
|
2631
2655
|
|
|
2632
2656
|
|
|
2657
|
+
|
|
@@ -2897,7 +2897,8 @@ registerNodeType("action.create_pr", {
|
|
|
2897
2897
|
// Build gh pr create command
|
|
2898
2898
|
const args = ["gh", "pr", "create"];
|
|
2899
2899
|
args.push("--title", JSON.stringify(title));
|
|
2900
|
-
|
|
2900
|
+
// gh pr create requires either --body (empty is allowed) or --fill* in non-interactive mode.
|
|
2901
|
+
args.push("--body", JSON.stringify(String(body)));
|
|
2901
2902
|
if (base) args.push("--base", base);
|
|
2902
2903
|
if (branch) args.push("--head", branch);
|
|
2903
2904
|
if (draft) args.push("--draft");
|
|
@@ -6893,6 +6894,7 @@ registerNodeType("transform.mcp_extract", {
|
|
|
6893
6894
|
|
|
6894
6895
|
/** Module-scope lazy caches for task lifecycle imports. */
|
|
6895
6896
|
let _taskClaimsMod = null;
|
|
6897
|
+
let _taskClaimsInitPromise = null;
|
|
6896
6898
|
let _taskComplexityMod = null;
|
|
6897
6899
|
let _kanbanAdapterMod = null;
|
|
6898
6900
|
let _agentPoolMod = null;
|
|
@@ -6903,6 +6905,46 @@ async function ensureTaskClaimsMod() {
|
|
|
6903
6905
|
if (!_taskClaimsMod) _taskClaimsMod = await import("../task/task-claims.mjs");
|
|
6904
6906
|
return _taskClaimsMod;
|
|
6905
6907
|
}
|
|
6908
|
+
function pickTaskString(...values) {
|
|
6909
|
+
for (const value of values) {
|
|
6910
|
+
const normalized = String(value || "").trim();
|
|
6911
|
+
if (normalized) return normalized;
|
|
6912
|
+
}
|
|
6913
|
+
return "";
|
|
6914
|
+
}
|
|
6915
|
+
function deriveTaskBranch(task = {}) {
|
|
6916
|
+
const explicit = pickTaskString(
|
|
6917
|
+
task?.branch,
|
|
6918
|
+
task?.branchName,
|
|
6919
|
+
task?.meta?.branch,
|
|
6920
|
+
task?.metadata?.branch,
|
|
6921
|
+
);
|
|
6922
|
+
if (explicit) return explicit;
|
|
6923
|
+
const taskId = pickTaskString(task?.id, task?.task_id).replace(/[^a-zA-Z0-9]/g, "").slice(0, 12);
|
|
6924
|
+
const titleSlug = pickTaskString(task?.title, "task")
|
|
6925
|
+
.toLowerCase()
|
|
6926
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6927
|
+
.replace(/^-+|-+$/g, "")
|
|
6928
|
+
.slice(0, 48);
|
|
6929
|
+
const suffix = titleSlug || "task";
|
|
6930
|
+
if (taskId) return `task/${taskId}-${suffix}`;
|
|
6931
|
+
return `task/${suffix}`;
|
|
6932
|
+
}
|
|
6933
|
+
async function ensureTaskClaimsInitialized(ctx, claims) {
|
|
6934
|
+
if (typeof claims?.initTaskClaims !== "function") return;
|
|
6935
|
+
if (!_taskClaimsInitPromise) {
|
|
6936
|
+
const repoRoot = pickTaskString(
|
|
6937
|
+
ctx?.data?.repoRoot,
|
|
6938
|
+
ctx?.data?.workspace,
|
|
6939
|
+
process.cwd(),
|
|
6940
|
+
);
|
|
6941
|
+
_taskClaimsInitPromise = claims.initTaskClaims({ repoRoot }).catch((err) => {
|
|
6942
|
+
_taskClaimsInitPromise = null;
|
|
6943
|
+
throw err;
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
await _taskClaimsInitPromise;
|
|
6947
|
+
}
|
|
6906
6948
|
async function ensureTaskComplexityMod() {
|
|
6907
6949
|
if (!_taskComplexityMod) _taskComplexityMod = await import("../task/task-complexity.mjs");
|
|
6908
6950
|
return _taskComplexityMod;
|
|
@@ -6937,6 +6979,7 @@ function normalizeCanStartGuardResult(raw) {
|
|
|
6937
6979
|
blockingTaskIds: [],
|
|
6938
6980
|
missingDependencyTaskIds: [],
|
|
6939
6981
|
blockingSprintIds: [],
|
|
6982
|
+
blockingEpicIds: [],
|
|
6940
6983
|
};
|
|
6941
6984
|
}
|
|
6942
6985
|
const data = raw && typeof raw === "object" ? raw : {};
|
|
@@ -6947,11 +6990,11 @@ function normalizeCanStartGuardResult(raw) {
|
|
|
6947
6990
|
blockingTaskIds: Array.isArray(data.blockingTaskIds) ? data.blockingTaskIds : [],
|
|
6948
6991
|
missingDependencyTaskIds: Array.isArray(data.missingDependencyTaskIds) ? data.missingDependencyTaskIds : [],
|
|
6949
6992
|
blockingSprintIds: Array.isArray(data.blockingSprintIds) ? data.blockingSprintIds : [],
|
|
6993
|
+
blockingEpicIds: Array.isArray(data.blockingEpicIds) ? data.blockingEpicIds : [],
|
|
6950
6994
|
sprintOrderMode: data.sprintOrderMode || null,
|
|
6951
6995
|
sprintTaskOrderMode: data.sprintTaskOrderMode || null,
|
|
6952
6996
|
};
|
|
6953
6997
|
}
|
|
6954
|
-
|
|
6955
6998
|
/** Resolve a config value, falling back to ctx.data, then defaultVal. */
|
|
6956
6999
|
function cfgOrCtx(node, ctx, key, defaultVal = "") {
|
|
6957
7000
|
const raw = node.config?.[key];
|
|
@@ -7153,6 +7196,7 @@ registerNodeType("trigger.task_available", {
|
|
|
7153
7196
|
blockingTaskIds: guard.blockingTaskIds,
|
|
7154
7197
|
missingDependencyTaskIds: guard.missingDependencyTaskIds,
|
|
7155
7198
|
blockingSprintIds: guard.blockingSprintIds,
|
|
7199
|
+
blockingEpicIds: guard.blockingEpicIds,
|
|
7156
7200
|
sprintOrderMode: guard.sprintOrderMode,
|
|
7157
7201
|
sprintTaskOrderMode: guard.sprintTaskOrderMode,
|
|
7158
7202
|
strict: Boolean(taskNotFound && strictStartGuardMissingTask),
|
|
@@ -7254,12 +7298,54 @@ registerNodeType("trigger.task_available", {
|
|
|
7254
7298
|
}
|
|
7255
7299
|
}
|
|
7256
7300
|
|
|
7301
|
+
const primaryTask = toDispatch[0] || null;
|
|
7302
|
+
if (primaryTask) {
|
|
7303
|
+
const taskId = pickTaskString(primaryTask.id, primaryTask.task_id);
|
|
7304
|
+
const taskTitle = pickTaskString(primaryTask.title, primaryTask.task_title);
|
|
7305
|
+
bindTaskContext(ctx, { taskId, taskTitle, task: primaryTask });
|
|
7306
|
+
const taskDescription = pickTaskString(
|
|
7307
|
+
primaryTask.description,
|
|
7308
|
+
primaryTask.task_description,
|
|
7309
|
+
);
|
|
7310
|
+
if (taskDescription) ctx.data.taskDescription = taskDescription;
|
|
7311
|
+
const taskWorkspace = pickTaskString(
|
|
7312
|
+
primaryTask.workspace,
|
|
7313
|
+
primaryTask.workspacePath,
|
|
7314
|
+
primaryTask.meta?.workspace,
|
|
7315
|
+
primaryTask.metadata?.workspace,
|
|
7316
|
+
);
|
|
7317
|
+
if (taskWorkspace) {
|
|
7318
|
+
ctx.data.workspace = taskWorkspace;
|
|
7319
|
+
if (!pickTaskString(ctx.data.repoRoot)) {
|
|
7320
|
+
ctx.data.repoRoot = taskWorkspace;
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7323
|
+
const taskRepository = pickTaskString(
|
|
7324
|
+
primaryTask.repository,
|
|
7325
|
+
primaryTask.repo,
|
|
7326
|
+
primaryTask.meta?.repository,
|
|
7327
|
+
primaryTask.metadata?.repository,
|
|
7328
|
+
);
|
|
7329
|
+
if (taskRepository) ctx.data.repository = taskRepository;
|
|
7330
|
+
const taskRepositories = Array.isArray(primaryTask.repositories)
|
|
7331
|
+
? primaryTask.repositories
|
|
7332
|
+
: [];
|
|
7333
|
+
if (taskRepositories.length > 0) {
|
|
7334
|
+
ctx.data.repositories = taskRepositories;
|
|
7335
|
+
}
|
|
7336
|
+
const baseBranch = pickTaskString(primaryTask.baseBranch, primaryTask.base_branch);
|
|
7337
|
+
if (baseBranch) ctx.data.baseBranch = baseBranch;
|
|
7338
|
+
const branch = deriveTaskBranch(primaryTask);
|
|
7339
|
+
if (branch) ctx.data.branch = branch;
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7257
7342
|
ctx.log(node.id, `Found ${toDispatch.length} task(s) ready (${remaining} slot(s) free)`);
|
|
7258
7343
|
return {
|
|
7259
7344
|
triggered: true,
|
|
7260
7345
|
tasks: toDispatch,
|
|
7261
7346
|
taskCount: toDispatch.length,
|
|
7262
7347
|
availableSlots: remaining,
|
|
7348
|
+
selectedTaskId: primaryTask ? pickTaskString(primaryTask.id, primaryTask.task_id) : "",
|
|
7263
7349
|
auditEvents: startGuardAuditEvents,
|
|
7264
7350
|
};
|
|
7265
7351
|
},
|
|
@@ -7426,6 +7512,12 @@ registerNodeType("action.claim_task", {
|
|
|
7426
7512
|
if (!taskId) throw new Error("action.claim_task: taskId is required");
|
|
7427
7513
|
|
|
7428
7514
|
const claims = await ensureTaskClaimsMod();
|
|
7515
|
+
try {
|
|
7516
|
+
await ensureTaskClaimsInitialized(ctx, claims);
|
|
7517
|
+
} catch (initErr) {
|
|
7518
|
+
ctx.log(node.id, `Claim init failed: ${initErr.message}`);
|
|
7519
|
+
return { success: false, error: initErr.message, taskId, alreadyClaimed: false };
|
|
7520
|
+
}
|
|
7429
7521
|
|
|
7430
7522
|
let claimResult;
|
|
7431
7523
|
try {
|
|
@@ -7522,6 +7614,14 @@ registerNodeType("action.release_claim", {
|
|
|
7522
7614
|
}
|
|
7523
7615
|
|
|
7524
7616
|
const claims = await ensureTaskClaimsMod();
|
|
7617
|
+
try {
|
|
7618
|
+
await ensureTaskClaimsInitialized(ctx, claims);
|
|
7619
|
+
} catch (initErr) {
|
|
7620
|
+
ctx.log(node.id, `Claim release init warning: ${initErr.message}`);
|
|
7621
|
+
ctx.data._claimToken = null;
|
|
7622
|
+
ctx.data._claimInstanceId = null;
|
|
7623
|
+
return { success: true, taskId, warning: initErr.message };
|
|
7624
|
+
}
|
|
7525
7625
|
try {
|
|
7526
7626
|
await claims.releaseTaskClaim({ taskId, claimToken, instanceId });
|
|
7527
7627
|
ctx.data._claimToken = null;
|
|
@@ -44,6 +44,14 @@ function getChildProcess() {
|
|
|
44
44
|
return _childProcessModule;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function sanitizeGitProcessEnv(baseEnv = process.env) {
|
|
48
|
+
const env = { ...baseEnv };
|
|
49
|
+
delete env.GIT_DIR;
|
|
50
|
+
delete env.GIT_WORK_TREE;
|
|
51
|
+
delete env.GIT_INDEX_FILE;
|
|
52
|
+
return env;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
// Lazy-loaded reference to repo-config.mjs (resolved on first use)
|
|
48
56
|
let _repoConfigModule = null;
|
|
49
57
|
|
|
@@ -247,6 +255,7 @@ function cloneIntoExistingRepoPath(childProcess, repoUrl, repoPath) {
|
|
|
247
255
|
timeout: 300000,
|
|
248
256
|
stdio: ["pipe", "pipe", "pipe"],
|
|
249
257
|
cwd: repoPath,
|
|
258
|
+
env: sanitizeGitProcessEnv(),
|
|
250
259
|
});
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -321,6 +330,7 @@ function buildGitPullFailureDetails(err, repoPath, childProcess) {
|
|
|
321
330
|
encoding: "utf8",
|
|
322
331
|
timeout: 10_000,
|
|
323
332
|
stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
env: sanitizeGitProcessEnv(),
|
|
324
334
|
}),
|
|
325
335
|
);
|
|
326
336
|
if (statusOut) {
|
|
@@ -571,6 +581,7 @@ export function addRepoToWorkspace(configDir, workspaceId, { url, name, branch,
|
|
|
571
581
|
encoding: "utf8",
|
|
572
582
|
timeout: 300000, // 5 minutes
|
|
573
583
|
stdio: ["pipe", "pipe", "pipe"],
|
|
584
|
+
env: sanitizeGitProcessEnv(),
|
|
574
585
|
});
|
|
575
586
|
|
|
576
587
|
if (result.status !== 0) {
|
|
@@ -698,6 +709,7 @@ export function pullWorkspaceRepos(configDir, workspaceId) {
|
|
|
698
709
|
encoding: "utf8",
|
|
699
710
|
timeout: 300000,
|
|
700
711
|
stdio: ["pipe", "pipe", "pipe"],
|
|
712
|
+
env: sanitizeGitProcessEnv(),
|
|
701
713
|
});
|
|
702
714
|
if (clone.status !== 0) {
|
|
703
715
|
const stderr = String(clone.stderr || clone.stdout || "");
|
|
@@ -787,6 +799,7 @@ export function pullWorkspaceRepos(configDir, workspaceId) {
|
|
|
787
799
|
encoding: "utf8",
|
|
788
800
|
timeout: 120000,
|
|
789
801
|
stdio: ["pipe", "pipe", "pipe"],
|
|
802
|
+
env: sanitizeGitProcessEnv(),
|
|
790
803
|
});
|
|
791
804
|
results.push({ name: repo.name, success: true });
|
|
792
805
|
} catch (err) {
|
|
@@ -939,6 +952,7 @@ export function detectWorkspaces(configDir) {
|
|
|
939
952
|
encoding: "utf8",
|
|
940
953
|
timeout: 3000,
|
|
941
954
|
stdio: ["pipe", "pipe", "ignore"],
|
|
955
|
+
env: sanitizeGitProcessEnv(),
|
|
942
956
|
}).trim();
|
|
943
957
|
slug = extractSlug(remote);
|
|
944
958
|
} catch { /* no remote */ }
|
|
@@ -65,7 +65,7 @@ const GIT_ENV = {
|
|
|
65
65
|
*/
|
|
66
66
|
function fixGitConfigCorruption(repoRoot) {
|
|
67
67
|
try {
|
|
68
|
-
const bareResult = spawnSync("git", ["config", "--
|
|
68
|
+
const bareResult = spawnSync("git", ["config", "--bool", "--get", "core.bare"], {
|
|
69
69
|
cwd: repoRoot,
|
|
70
70
|
encoding: "utf8",
|
|
71
71
|
timeout: 5000,
|
|
@@ -75,7 +75,7 @@ function fixGitConfigCorruption(repoRoot) {
|
|
|
75
75
|
console.warn(
|
|
76
76
|
`${TAG} :alert: Detected core.bare=true on main repo — fixing git config corruption`,
|
|
77
77
|
);
|
|
78
|
-
spawnSync("git", ["config", "--
|
|
78
|
+
spawnSync("git", ["config", "--local", "core.bare", "false"], {
|
|
79
79
|
cwd: repoRoot,
|
|
80
80
|
encoding: "utf8",
|
|
81
81
|
timeout: 5000,
|