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
|
@@ -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);
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7456
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
7710
|
-
throw new Error(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
578
|
+
workflowfirst: Object.freeze({
|
|
579
579
|
id: "workflowFirst",
|
|
580
580
|
name: "Workflow-First (Full)",
|
|
581
581
|
description:
|