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.
@@ -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);
@@ -2442,18 +2466,6 @@ export class WorkflowEngine extends EventEmitter {
2442
2466
 
2443
2467
  if (!runs.length) {
2444
2468
  this._resumingRuns = false;
2445
- this._taskTraceHooks = new Set();
2446
- if (typeof opts.onTaskWorkflowEvent === "function") {
2447
- this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
2448
- }
2449
- if (typeof opts.taskTraceHook === "function") {
2450
- this._taskTraceHooks.add(opts.taskTraceHook);
2451
- }
2452
- if (Array.isArray(opts.taskTraceHooks)) {
2453
- for (const hook of opts.taskTraceHooks) {
2454
- if (typeof hook === "function") this._taskTraceHooks.add(hook);
2455
- }
2456
- }
2457
2469
  return;
2458
2470
  }
2459
2471
 
@@ -2512,18 +2524,6 @@ export class WorkflowEngine extends EventEmitter {
2512
2524
  }
2513
2525
  } finally {
2514
2526
  this._resumingRuns = false;
2515
- this._taskTraceHooks = new Set();
2516
- if (typeof opts.onTaskWorkflowEvent === "function") {
2517
- this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
2518
- }
2519
- if (typeof opts.taskTraceHook === "function") {
2520
- this._taskTraceHooks.add(opts.taskTraceHook);
2521
- }
2522
- if (Array.isArray(opts.taskTraceHooks)) {
2523
- for (const hook of opts.taskTraceHooks) {
2524
- if (typeof hook === "function") this._taskTraceHooks.add(hook);
2525
- }
2526
- }
2527
2527
  }
2528
2528
  }
2529
2529
 
@@ -2630,3 +2630,4 @@ export async function executeWorkflow(id, data, opts) { return getWorkflowEngine
2630
2630
  export async function retryWorkflowRun(runId, retryOpts, engineOpts) { return getWorkflowEngine(engineOpts).retryRun(runId, retryOpts); }
2631
2631
 
2632
2632
 
2633
+
@@ -25,6 +25,8 @@ import { randomUUID } from "node:crypto";
25
25
  import { getAgentToolConfig, getEffectiveTools } from "../agent/agent-tool-config.mjs";
26
26
  import { getToolsPromptBlock } from "../agent/agent-custom-tools.mjs";
27
27
  import { buildRelevantSkillsPromptBlock, findRelevantSkills } from "../agent/bosun-skills.mjs";
28
+ import { getSessionTracker } from "../infra/session-tracker.mjs";
29
+ import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
28
30
 
29
31
  const TAG = "[workflow-nodes]";
30
32
  const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
@@ -40,6 +42,29 @@ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
40
42
  return Math.max(20, Math.min(500, Math.trunc(raw)));
41
43
  })();
42
44
 
45
+ function makeIsolatedGitEnv(extra = {}) {
46
+ const env = { ...process.env, ...extra };
47
+ for (const key of [
48
+ "GIT_DIR",
49
+ "GIT_WORK_TREE",
50
+ "GIT_COMMON_DIR",
51
+ "GIT_INDEX_FILE",
52
+ "GIT_OBJECT_DIRECTORY",
53
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
54
+ "GIT_PREFIX",
55
+ ]) {
56
+ delete env[key];
57
+ }
58
+ return env;
59
+ }
60
+
61
+ function execGitSync(command, options = {}) {
62
+ return execSync(command, {
63
+ ...options,
64
+ env: makeIsolatedGitEnv(options.env),
65
+ });
66
+ }
67
+
43
68
  function trimLogText(value, max = 180) {
44
69
  const text = String(value || "").replace(/\s+/g, " ").trim();
45
70
  if (!text) return "";
@@ -1510,6 +1535,20 @@ registerNodeType("action.run_agent", {
1510
1535
  const prompt = ctx.resolve(node.config?.prompt || "");
1511
1536
  const sdk = node.config?.sdk || "auto";
1512
1537
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
1538
+ const trackedTaskId = String(
1539
+ ctx.data?.taskId ||
1540
+ ctx.data?.task?.id ||
1541
+ ctx.data?.taskDetail?.id ||
1542
+ ctx.resolve(node.config?.taskId || "") ||
1543
+ "",
1544
+ ).trim();
1545
+ const trackedTaskTitle = String(
1546
+ ctx.data?.task?.title ||
1547
+ ctx.data?.taskDetail?.title ||
1548
+ ctx.data?.taskInfo?.title ||
1549
+ trackedTaskId ||
1550
+ "",
1551
+ ).trim();
1513
1552
  const agentProfileId = String(
1514
1553
  ctx.resolve(node.config?.agentProfile || ctx.data?.agentProfile || ""),
1515
1554
  ).trim();
@@ -1637,6 +1676,43 @@ registerNodeType("action.run_agent", {
1637
1676
  const maxRetainedEvents = Number.isFinite(Number(node.config?.maxRetainedEvents))
1638
1677
  ? Math.max(10, Math.min(500, Math.trunc(Number(node.config.maxRetainedEvents))))
1639
1678
  : WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT;
1679
+ const tracker = trackedTaskId ? getSessionTracker() : null;
1680
+ const trackedSessionType = trackedTaskId ? "task" : "flow";
1681
+
1682
+ if (tracker && trackedTaskId) {
1683
+ const existing = tracker.getSessionById(trackedTaskId);
1684
+ if (!existing) {
1685
+ tracker.createSession({
1686
+ id: trackedTaskId,
1687
+ type: "task",
1688
+ taskId: trackedTaskId,
1689
+ metadata: {
1690
+ title: trackedTaskTitle || trackedTaskId,
1691
+ workspaceId: String(ctx.data?.workspaceId || ctx.data?.activeWorkspace || "").trim() || undefined,
1692
+ workspaceDir: String(cwd || "").trim() || undefined,
1693
+ branch:
1694
+ String(
1695
+ ctx.data?.branch ||
1696
+ ctx.data?.task?.branchName ||
1697
+ ctx.data?.taskDetail?.branchName ||
1698
+ "",
1699
+ ).trim() || undefined,
1700
+ },
1701
+ });
1702
+ } else {
1703
+ tracker.updateSessionStatus(trackedTaskId, "active");
1704
+ if (trackedTaskTitle) {
1705
+ tracker.renameSession(trackedTaskId, trackedTaskTitle);
1706
+ }
1707
+ }
1708
+ tracker.recordEvent(trackedTaskId, {
1709
+ role: "system",
1710
+ type: "system",
1711
+ content: `Workflow agent run started in ${cwd}`,
1712
+ timestamp: new Date().toISOString(),
1713
+ _sessionType: trackedSessionType,
1714
+ });
1715
+ }
1640
1716
 
1641
1717
  const launchExtra = {};
1642
1718
  if (sessionId) launchExtra.resumeThreadId = sessionId;
@@ -1644,6 +1720,12 @@ registerNodeType("action.run_agent", {
1644
1720
  if (modelOverride) launchExtra.model = modelOverride;
1645
1721
  launchExtra.onEvent = (event) => {
1646
1722
  try {
1723
+ if (tracker && trackedTaskId) {
1724
+ tracker.recordEvent(trackedTaskId, {
1725
+ ...(event && typeof event === "object" ? event : { content: String(event || "") }),
1726
+ _sessionType: trackedSessionType,
1727
+ });
1728
+ }
1647
1729
  const line = summarizeAgentStreamEvent(event);
1648
1730
  if (!line || line === lastStreamLog) return;
1649
1731
  lastStreamLog = line;
@@ -1711,9 +1793,10 @@ registerNodeType("action.run_agent", {
1711
1793
  timeoutMs,
1712
1794
  maxRetries: sessionRetries,
1713
1795
  maxContinues,
1714
- sessionType: "flow",
1796
+ sessionType: trackedSessionType,
1715
1797
  sdk: sdkOverride,
1716
1798
  model: modelOverride,
1799
+ onEvent: launchExtra.onEvent,
1717
1800
  });
1718
1801
  }
1719
1802
 
@@ -1721,9 +1804,10 @@ registerNodeType("action.run_agent", {
1721
1804
  ctx.log(node.id, `${passLabel} Recovery: launchOrResumeThread taskKey=${recoveryTaskKey}`.trim());
1722
1805
  result = await agentPool.launchOrResumeThread(passPrompt, cwd, timeoutMs, {
1723
1806
  taskKey: recoveryTaskKey,
1724
- sessionType: "flow",
1807
+ sessionType: trackedSessionType,
1725
1808
  sdk: sdkOverride,
1726
1809
  model: modelOverride,
1810
+ onEvent: launchExtra.onEvent,
1727
1811
  });
1728
1812
  }
1729
1813
 
@@ -1736,6 +1820,24 @@ registerNodeType("action.run_agent", {
1736
1820
  }
1737
1821
  ctx.log(node.id, `${passLabel || "Agent"} completed: success=${success} streamEvents=${streamEventCount}`);
1738
1822
 
1823
+ if (tracker && trackedTaskId) {
1824
+ if (streamEventCount === 0) {
1825
+ const fallbackContent = success
1826
+ ? String(result?.output || result?.message || "Agent run completed.").trim()
1827
+ : String(result?.error || "Agent run failed.").trim();
1828
+ if (fallbackContent) {
1829
+ tracker.recordEvent(trackedTaskId, {
1830
+ role: success ? "assistant" : "system",
1831
+ type: success ? "agent_message" : "error",
1832
+ content: fallbackContent,
1833
+ timestamp: new Date().toISOString(),
1834
+ _sessionType: trackedSessionType,
1835
+ });
1836
+ }
1837
+ }
1838
+ tracker.endSession(trackedTaskId, success ? "completed" : "failed");
1839
+ }
1840
+
1739
1841
  const threadId = result?.threadId || result?.sessionId || sessionId || null;
1740
1842
  if (persistSession && threadId) {
1741
1843
  ctx.data.sessionId = threadId;
@@ -2713,7 +2815,7 @@ registerNodeType("action.update_task_status", {
2713
2815
  required: ["taskId", "status"],
2714
2816
  },
2715
2817
  async execute(node, ctx, engine) {
2716
- const taskId = ctx.resolve(node.config?.taskId || "");
2818
+ let taskId = ctx.resolve(node.config?.taskId || "");
2717
2819
  const status = node.config?.status;
2718
2820
  const kanban = engine.services?.kanban;
2719
2821
  const workflowEvent = ctx.resolve(node.config?.workflowEvent || "");
@@ -2732,6 +2834,29 @@ registerNodeType("action.update_task_status", {
2732
2834
  if (workflowData) updateOptions.workflowData = workflowData;
2733
2835
  if (workflowDedupKey) updateOptions.workflowDedupKey = workflowDedupKey;
2734
2836
 
2837
+ if (isUnresolvedTemplateToken(taskId)) {
2838
+ const fallbackTaskId =
2839
+ ctx.data?.taskId ||
2840
+ ctx.data?.task?.id ||
2841
+ ctx.data?.task_id ||
2842
+ "";
2843
+ if (fallbackTaskId && !isUnresolvedTemplateToken(fallbackTaskId)) {
2844
+ taskId = String(fallbackTaskId);
2845
+ }
2846
+ }
2847
+
2848
+ if (!taskId || isUnresolvedTemplateToken(taskId)) {
2849
+ const unresolvedValue = String(taskId || node.config?.taskId || "(empty)");
2850
+ ctx.log(node.id, `Skipping update_task_status due unresolved taskId: ${unresolvedValue}`);
2851
+ return {
2852
+ success: false,
2853
+ skipped: true,
2854
+ error: "unresolved_task_id",
2855
+ taskId: unresolvedValue,
2856
+ status,
2857
+ };
2858
+ }
2859
+
2735
2860
  if (kanban?.updateTaskStatus) {
2736
2861
  await kanban.updateTaskStatus(taskId, status, updateOptions);
2737
2862
  bindTaskContext(ctx, {
@@ -2897,7 +3022,8 @@ registerNodeType("action.create_pr", {
2897
3022
  // Build gh pr create command
2898
3023
  const args = ["gh", "pr", "create"];
2899
3024
  args.push("--title", JSON.stringify(title));
2900
- if (body) args.push("--body", JSON.stringify(body));
3025
+ // gh pr create requires either --body (empty is allowed) or --fill* in non-interactive mode.
3026
+ args.push("--body", JSON.stringify(String(body)));
2901
3027
  if (base) args.push("--base", base);
2902
3028
  if (branch) args.push("--head", branch);
2903
3029
  if (draft) args.push("--draft");
@@ -6893,6 +7019,7 @@ registerNodeType("transform.mcp_extract", {
6893
7019
 
6894
7020
  /** Module-scope lazy caches for task lifecycle imports. */
6895
7021
  let _taskClaimsMod = null;
7022
+ let _taskClaimsInitPromise = null;
6896
7023
  let _taskComplexityMod = null;
6897
7024
  let _kanbanAdapterMod = null;
6898
7025
  let _agentPoolMod = null;
@@ -6903,6 +7030,46 @@ async function ensureTaskClaimsMod() {
6903
7030
  if (!_taskClaimsMod) _taskClaimsMod = await import("../task/task-claims.mjs");
6904
7031
  return _taskClaimsMod;
6905
7032
  }
7033
+ function pickTaskString(...values) {
7034
+ for (const value of values) {
7035
+ const normalized = String(value || "").trim();
7036
+ if (normalized) return normalized;
7037
+ }
7038
+ return "";
7039
+ }
7040
+ function deriveTaskBranch(task = {}) {
7041
+ const explicit = pickTaskString(
7042
+ task?.branch,
7043
+ task?.branchName,
7044
+ task?.meta?.branch,
7045
+ task?.metadata?.branch,
7046
+ );
7047
+ if (explicit) return explicit;
7048
+ const taskId = pickTaskString(task?.id, task?.task_id).replace(/[^a-zA-Z0-9]/g, "").slice(0, 12);
7049
+ const titleSlug = pickTaskString(task?.title, "task")
7050
+ .toLowerCase()
7051
+ .replace(/[^a-z0-9]+/g, "-")
7052
+ .replace(/^-+|-+$/g, "")
7053
+ .slice(0, 48);
7054
+ const suffix = titleSlug || "task";
7055
+ if (taskId) return `task/${taskId}-${suffix}`;
7056
+ return `task/${suffix}`;
7057
+ }
7058
+ async function ensureTaskClaimsInitialized(ctx, claims) {
7059
+ if (typeof claims?.initTaskClaims !== "function") return;
7060
+ if (!_taskClaimsInitPromise) {
7061
+ const repoRoot = pickTaskString(
7062
+ ctx?.data?.repoRoot,
7063
+ ctx?.data?.workspace,
7064
+ process.cwd(),
7065
+ );
7066
+ _taskClaimsInitPromise = claims.initTaskClaims({ repoRoot }).catch((err) => {
7067
+ _taskClaimsInitPromise = null;
7068
+ throw err;
7069
+ });
7070
+ }
7071
+ await _taskClaimsInitPromise;
7072
+ }
6906
7073
  async function ensureTaskComplexityMod() {
6907
7074
  if (!_taskComplexityMod) _taskComplexityMod = await import("../task/task-complexity.mjs");
6908
7075
  return _taskComplexityMod;
@@ -6937,6 +7104,7 @@ function normalizeCanStartGuardResult(raw) {
6937
7104
  blockingTaskIds: [],
6938
7105
  missingDependencyTaskIds: [],
6939
7106
  blockingSprintIds: [],
7107
+ blockingEpicIds: [],
6940
7108
  };
6941
7109
  }
6942
7110
  const data = raw && typeof raw === "object" ? raw : {};
@@ -6947,11 +7115,11 @@ function normalizeCanStartGuardResult(raw) {
6947
7115
  blockingTaskIds: Array.isArray(data.blockingTaskIds) ? data.blockingTaskIds : [],
6948
7116
  missingDependencyTaskIds: Array.isArray(data.missingDependencyTaskIds) ? data.missingDependencyTaskIds : [],
6949
7117
  blockingSprintIds: Array.isArray(data.blockingSprintIds) ? data.blockingSprintIds : [],
7118
+ blockingEpicIds: Array.isArray(data.blockingEpicIds) ? data.blockingEpicIds : [],
6950
7119
  sprintOrderMode: data.sprintOrderMode || null,
6951
7120
  sprintTaskOrderMode: data.sprintTaskOrderMode || null,
6952
7121
  };
6953
7122
  }
6954
-
6955
7123
  /** Resolve a config value, falling back to ctx.data, then defaultVal. */
6956
7124
  function cfgOrCtx(node, ctx, key, defaultVal = "") {
6957
7125
  const raw = node.config?.[key];
@@ -6961,6 +7129,48 @@ function cfgOrCtx(node, ctx, key, defaultVal = "") {
6961
7129
  return defaultVal;
6962
7130
  }
6963
7131
 
7132
+ function getWorkflowRuntimeState(ctx) {
7133
+ if (!ctx || typeof ctx !== "object") return {};
7134
+ if (!ctx.__workflowRuntimeState || typeof ctx.__workflowRuntimeState !== "object") {
7135
+ ctx.__workflowRuntimeState = {};
7136
+ }
7137
+ return ctx.__workflowRuntimeState;
7138
+ }
7139
+
7140
+ function isUnresolvedTemplateToken(value) {
7141
+ return /{{[^{}]+}}/.test(String(value || ""));
7142
+ }
7143
+
7144
+ function normalizeGitRefValue(value) {
7145
+ const text = String(value ?? "").trim();
7146
+ if (!text || isUnresolvedTemplateToken(text)) return "";
7147
+ const lowered = text.toLowerCase();
7148
+ if (lowered === "null" || lowered === "undefined") return "";
7149
+ return text;
7150
+ }
7151
+
7152
+ function pickGitRef(...candidates) {
7153
+ for (const candidate of candidates) {
7154
+ const normalized = normalizeGitRefValue(candidate);
7155
+ if (normalized) return normalized;
7156
+ }
7157
+ return "";
7158
+ }
7159
+
7160
+ function formatExecSyncError(err) {
7161
+ if (!err) return "unknown error";
7162
+ const detail = [err?.stderr, err?.stdout, err?.message]
7163
+ .map((entry) => String(entry || "").trim())
7164
+ .filter(Boolean)
7165
+ .join(" | ");
7166
+ return trimLogText(detail || String(err?.message || err), 420);
7167
+ }
7168
+
7169
+ function isExistingBranchWorktreeError(err) {
7170
+ const detail = formatExecSyncError(err).toLowerCase();
7171
+ return detail.includes("already exists") || detail.includes("is already checked out");
7172
+ }
7173
+
6964
7174
  /**
6965
7175
  * Anti-thrash state — module-scope to survive across workflow runs.
6966
7176
  * Mirrors TaskExecutor._noCommitCounts / _skipUntil / _completedWithPR.
@@ -7153,6 +7363,7 @@ registerNodeType("trigger.task_available", {
7153
7363
  blockingTaskIds: guard.blockingTaskIds,
7154
7364
  missingDependencyTaskIds: guard.missingDependencyTaskIds,
7155
7365
  blockingSprintIds: guard.blockingSprintIds,
7366
+ blockingEpicIds: guard.blockingEpicIds,
7156
7367
  sprintOrderMode: guard.sprintOrderMode,
7157
7368
  sprintTaskOrderMode: guard.sprintTaskOrderMode,
7158
7369
  strict: Boolean(taskNotFound && strictStartGuardMissingTask),
@@ -7254,12 +7465,54 @@ registerNodeType("trigger.task_available", {
7254
7465
  }
7255
7466
  }
7256
7467
 
7468
+ const primaryTask = toDispatch[0] || null;
7469
+ if (primaryTask) {
7470
+ const taskId = pickTaskString(primaryTask.id, primaryTask.task_id);
7471
+ const taskTitle = pickTaskString(primaryTask.title, primaryTask.task_title);
7472
+ bindTaskContext(ctx, { taskId, taskTitle, task: primaryTask });
7473
+ const taskDescription = pickTaskString(
7474
+ primaryTask.description,
7475
+ primaryTask.task_description,
7476
+ );
7477
+ if (taskDescription) ctx.data.taskDescription = taskDescription;
7478
+ const taskWorkspace = pickTaskString(
7479
+ primaryTask.workspace,
7480
+ primaryTask.workspacePath,
7481
+ primaryTask.meta?.workspace,
7482
+ primaryTask.metadata?.workspace,
7483
+ );
7484
+ if (taskWorkspace) {
7485
+ ctx.data.workspace = taskWorkspace;
7486
+ if (!pickTaskString(ctx.data.repoRoot)) {
7487
+ ctx.data.repoRoot = taskWorkspace;
7488
+ }
7489
+ }
7490
+ const taskRepository = pickTaskString(
7491
+ primaryTask.repository,
7492
+ primaryTask.repo,
7493
+ primaryTask.meta?.repository,
7494
+ primaryTask.metadata?.repository,
7495
+ );
7496
+ if (taskRepository) ctx.data.repository = taskRepository;
7497
+ const taskRepositories = Array.isArray(primaryTask.repositories)
7498
+ ? primaryTask.repositories
7499
+ : [];
7500
+ if (taskRepositories.length > 0) {
7501
+ ctx.data.repositories = taskRepositories;
7502
+ }
7503
+ const baseBranch = pickTaskString(primaryTask.baseBranch, primaryTask.base_branch);
7504
+ if (baseBranch) ctx.data.baseBranch = baseBranch;
7505
+ const branch = deriveTaskBranch(primaryTask);
7506
+ if (branch) ctx.data.branch = branch;
7507
+ }
7508
+
7257
7509
  ctx.log(node.id, `Found ${toDispatch.length} task(s) ready (${remaining} slot(s) free)`);
7258
7510
  return {
7259
7511
  triggered: true,
7260
7512
  tasks: toDispatch,
7261
7513
  taskCount: toDispatch.length,
7262
7514
  availableSlots: remaining,
7515
+ selectedTaskId: primaryTask ? pickTaskString(primaryTask.id, primaryTask.task_id) : "",
7263
7516
  auditEvents: startGuardAuditEvents,
7264
7517
  };
7265
7518
  },
@@ -7426,6 +7679,12 @@ registerNodeType("action.claim_task", {
7426
7679
  if (!taskId) throw new Error("action.claim_task: taskId is required");
7427
7680
 
7428
7681
  const claims = await ensureTaskClaimsMod();
7682
+ try {
7683
+ await ensureTaskClaimsInitialized(ctx, claims);
7684
+ } catch (initErr) {
7685
+ ctx.log(node.id, `Claim init failed: ${initErr.message}`);
7686
+ return { success: false, error: initErr.message, taskId, alreadyClaimed: false };
7687
+ }
7429
7688
 
7430
7689
  let claimResult;
7431
7690
  try {
@@ -7452,11 +7711,18 @@ registerNodeType("action.claim_task", {
7452
7711
  ctx.data._claimToken = token;
7453
7712
  ctx.data._claimInstanceId = instanceId;
7454
7713
 
7455
- // Start renewal timer (stored in ctx for cleanup by release_claim)
7456
- if (renewIntervalMs > 0 && claims.renewTaskClaim) {
7714
+ const runtimeState = getWorkflowRuntimeState(ctx);
7715
+ // Start renewal timer (stored in non-serializable runtime state for cleanup by release_claim)
7716
+ const renewClaimFn =
7717
+ typeof claims.renewTaskClaim === "function"
7718
+ ? claims.renewTaskClaim.bind(claims)
7719
+ : typeof claims.renewClaim === "function"
7720
+ ? claims.renewClaim.bind(claims)
7721
+ : null;
7722
+ if (renewIntervalMs > 0 && renewClaimFn) {
7457
7723
  const renewTimer = setInterval(async () => {
7458
7724
  try {
7459
- await claims.renewTaskClaim({ taskId, claimToken: token, instanceId, ttlMinutes });
7725
+ await renewClaimFn({ taskId, claimToken: token, instanceId, ttlMinutes });
7460
7726
  } catch (renewErr) {
7461
7727
  const msg = renewErr?.message || String(renewErr);
7462
7728
  const fatal = ["claimed_by_different_instance", "claim_token_mismatch",
@@ -7464,6 +7730,7 @@ registerNodeType("action.claim_task", {
7464
7730
  if (fatal) {
7465
7731
  ctx.log(node.id, `Claim renewal fatal: ${msg} — aborting task`);
7466
7732
  clearInterval(renewTimer);
7733
+ runtimeState.claimRenewTimer = null;
7467
7734
  ctx.data._claimRenewTimer = null;
7468
7735
  // Signal abort to downstream nodes via context
7469
7736
  ctx.data._claimStolen = true;
@@ -7474,7 +7741,9 @@ registerNodeType("action.claim_task", {
7474
7741
  }, renewIntervalMs);
7475
7742
  // Prevent timer from keeping the process alive
7476
7743
  if (renewTimer.unref) renewTimer.unref();
7477
- ctx.data._claimRenewTimer = renewTimer;
7744
+ runtimeState.claimRenewTimer = renewTimer;
7745
+ // Keep serialized context JSON-safe.
7746
+ ctx.data._claimRenewTimer = null;
7478
7747
  }
7479
7748
 
7480
7749
  ctx.log(node.id, `Task "${taskTitle}" claimed (ttl=${ttlMinutes}min, renew=${renewIntervalMs}ms)`);
@@ -7510,11 +7779,14 @@ registerNodeType("action.release_claim", {
7510
7779
  const claimToken = cfgOrCtx(node, ctx, "claimToken") || ctx.data?._claimToken || "";
7511
7780
  const instanceId = cfgOrCtx(node, ctx, "instanceId") || ctx.data?._claimInstanceId || "";
7512
7781
 
7513
- // Always cancel the renewal timer first
7514
- if (ctx.data?._claimRenewTimer) {
7515
- try { clearInterval(ctx.data._claimRenewTimer); } catch { /* ok */ }
7516
- ctx.data._claimRenewTimer = null;
7782
+ // Always cancel the renewal timer first.
7783
+ const runtimeState = getWorkflowRuntimeState(ctx);
7784
+ const renewTimer = runtimeState.claimRenewTimer || ctx.data?._claimRenewTimer;
7785
+ if (renewTimer) {
7786
+ try { clearInterval(renewTimer); } catch { /* ok */ }
7517
7787
  }
7788
+ runtimeState.claimRenewTimer = null;
7789
+ ctx.data._claimRenewTimer = null;
7518
7790
 
7519
7791
  if (!taskId || !claimToken) {
7520
7792
  ctx.log(node.id, `No claim to release for ${taskId || "(unknown)"}`);
@@ -7523,7 +7795,22 @@ registerNodeType("action.release_claim", {
7523
7795
 
7524
7796
  const claims = await ensureTaskClaimsMod();
7525
7797
  try {
7526
- await claims.releaseTaskClaim({ taskId, claimToken, instanceId });
7798
+ await ensureTaskClaimsInitialized(ctx, claims);
7799
+ } catch (initErr) {
7800
+ ctx.log(node.id, `Claim release init warning: ${initErr.message}`);
7801
+ ctx.data._claimToken = null;
7802
+ ctx.data._claimInstanceId = null;
7803
+ return { success: true, taskId, warning: initErr.message };
7804
+ }
7805
+ const releaseClaimFn =
7806
+ typeof claims.releaseTaskClaim === "function"
7807
+ ? claims.releaseTaskClaim.bind(claims)
7808
+ : typeof claims.releaseTask === "function"
7809
+ ? claims.releaseTask.bind(claims)
7810
+ : null;
7811
+ try {
7812
+ if (!releaseClaimFn) throw new Error("no claim release function available");
7813
+ await releaseClaimFn({ taskId, claimToken, instanceId });
7527
7814
  ctx.data._claimToken = null;
7528
7815
  ctx.data._claimInstanceId = null;
7529
7816
  ctx.log(node.id, `Claim released for ${taskId}`);
@@ -7642,7 +7929,9 @@ registerNodeType("action.acquire_worktree", {
7642
7929
  const taskId = cfgOrCtx(node, ctx, "taskId");
7643
7930
  const branch = cfgOrCtx(node, ctx, "branch");
7644
7931
  const repoRoot = cfgOrCtx(node, ctx, "repoRoot") || process.cwd();
7645
- const baseBranch = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
7932
+ const baseBranchRaw = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
7933
+ const defaultTargetBranch = cfgOrCtx(node, ctx, "defaultTargetBranch", "origin/main");
7934
+ const baseBranch = pickGitRef(baseBranchRaw, defaultTargetBranch, "origin/main", "main");
7646
7935
  const fetchTimeout = node.config?.fetchTimeout ?? 30000;
7647
7936
  const worktreeTimeout = node.config?.worktreeTimeout ?? 60000;
7648
7937
 
@@ -7699,17 +7988,24 @@ registerNodeType("action.acquire_worktree", {
7699
7988
  `git worktree add "${worktreePath}" -b "${branch}" "${baseBranch}" 2>&1`,
7700
7989
  { cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
7701
7990
  );
7702
- } catch {
7703
- // Branch may already exist — try checkout
7991
+ } catch (createErr) {
7992
+ if (!isExistingBranchWorktreeError(createErr)) {
7993
+ throw new Error(`Worktree creation failed: ${formatExecSyncError(createErr)}`);
7994
+ }
7995
+ // Branch already exists — attach worktree to existing branch.
7704
7996
  try {
7705
7997
  execSync(
7706
7998
  `git worktree add "${worktreePath}" "${branch}" 2>&1`,
7707
7999
  { cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
7708
8000
  );
7709
- } catch (err2) {
7710
- throw new Error(`Worktree creation failed: ${err2.message}`);
8001
+ } catch (reuseErr) {
8002
+ throw new Error(
8003
+ `Worktree creation failed: ${formatExecSyncError(createErr)}; ` +
8004
+ `reuse failed: ${formatExecSyncError(reuseErr)}`,
8005
+ );
7711
8006
  }
7712
8007
  }
8008
+ fixGitConfigCorruption(repoRoot);
7713
8009
 
7714
8010
  ctx.data.worktreePath = worktreePath;
7715
8011
  ctx.data._worktreeCreated = true;
@@ -8104,7 +8400,7 @@ registerNodeType("action.detect_new_commits", {
8104
8400
  // Get current HEAD
8105
8401
  let postExecHead = "";
8106
8402
  try {
8107
- postExecHead = execSync("git rev-parse HEAD", {
8403
+ postExecHead = execGitSync("git rev-parse HEAD", {
8108
8404
  cwd: worktreePath, encoding: "utf8", timeout: 5000,
8109
8405
  }).trim();
8110
8406
  } catch (err) {
@@ -8118,7 +8414,7 @@ registerNodeType("action.detect_new_commits", {
8118
8414
  let hasUnpushed = false;
8119
8415
  let commitCount = 0;
8120
8416
  try {
8121
- const log = execSync(`git log --oneline ${baseBranch}..HEAD`, {
8417
+ const log = execGitSync(`git log --oneline ${baseBranch}..HEAD`, {
8122
8418
  cwd: worktreePath, encoding: "utf8", timeout: 10000,
8123
8419
  stdio: ["ignore", "pipe", "pipe"],
8124
8420
  }).trim();
@@ -8132,7 +8428,7 @@ registerNodeType("action.detect_new_commits", {
8132
8428
  let diffStats = null;
8133
8429
  if (hasNewCommits || hasUnpushed) {
8134
8430
  try {
8135
- const statOutput = execSync(`git diff --stat ${baseBranch}..HEAD`, {
8431
+ const statOutput = execGitSync(`git diff --stat ${baseBranch}..HEAD`, {
8136
8432
  cwd: worktreePath, encoding: "utf8", timeout: 10000,
8137
8433
  stdio: ["ignore", "pipe", "pipe"],
8138
8434
  }).trim();
@@ -8502,3 +8798,6 @@ registerNodeType("action.web_search", {
8502
8798
  // ═══════════════════════════════════════════════════════════════════════════
8503
8799
 
8504
8800
  export { registerNodeType, getNodeType, listNodeTypes } from "./workflow-engine.mjs";
8801
+
8802
+
8803
+
@@ -575,7 +575,7 @@ export const WORKFLOW_SETUP_PROFILES = Object.freeze({
575
575
  "template-task-batch-processor",
576
576
  ]),
577
577
  }),
578
- workflowFirst: Object.freeze({
578
+ workflowfirst: Object.freeze({
579
579
  id: "workflowFirst",
580
580
  name: "Workflow-First (Full)",
581
581
  description: