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.
@@ -1282,12 +1282,12 @@ export class WorkflowEngine extends EventEmitter {
1282
1282
  // ── Schedule trigger evaluation ──────────────────────────────────────────
1283
1283
 
1284
1284
  /**
1285
- * Evaluate all workflows that use `trigger.schedule` or `trigger.scheduled_once`.
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
- * schedule interval has elapsed since their last completed run.
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
- (n) => n.type === "trigger.schedule" || n.type === "trigger.scheduled_once",
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
- const intervalMs = Number(tNode.config?.intervalMs) || 3600000;
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
- if (body) args.push("--body", JSON.stringify(body));
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", "--local", "core.bare"], {
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", "--unset", "core.bare"], {
78
+ spawnSync("git", ["config", "--local", "core.bare", "false"], {
79
79
  cwd: repoRoot,
80
80
  encoding: "utf8",
81
81
  timeout: 5000,