bosun 0.41.0 → 0.41.2
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/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* schema → object — JSON Schema for node config
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getNodeType,
|
|
22
|
+
listNodeTypes,
|
|
23
|
+
NodeStatus,
|
|
24
|
+
registerNodeType,
|
|
25
|
+
unregisterNodeType,
|
|
26
|
+
} from "./workflow-engine.mjs";
|
|
21
27
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
22
28
|
import { resolve, dirname } from "node:path";
|
|
23
29
|
import { execSync, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -35,8 +41,18 @@ import {
|
|
|
35
41
|
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
36
42
|
import { clearBlockedWorktreeIdentity } from "../git/git-safety.mjs";
|
|
37
43
|
import { getGitHubToken, invalidateTokenType } from "../github/github-auth-manager.mjs";
|
|
44
|
+
import {
|
|
45
|
+
CUSTOM_NODE_DIR_NAME,
|
|
46
|
+
ensureCustomWorkflowNodesLoaded,
|
|
47
|
+
getCustomNodeDir,
|
|
48
|
+
scaffoldCustomNodeFile,
|
|
49
|
+
startCustomNodeDiscovery,
|
|
50
|
+
stopCustomNodeDiscovery,
|
|
51
|
+
} from "./workflow-nodes/custom-loader.mjs";
|
|
38
52
|
|
|
39
53
|
const TAG = "[workflow-nodes]";
|
|
54
|
+
let customLoadPromise = null;
|
|
55
|
+
let customDiscoveryStarted = false;
|
|
40
56
|
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');\"";
|
|
41
57
|
const PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND = "node -e \"const cp=require('node:child_process');cp.execSync('git worktree prune',{stdio:'ignore'});const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
42
58
|
const WORKFLOW_AGENT_HEARTBEAT_MS = (() => {
|
|
@@ -51,6 +67,394 @@ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
|
|
|
51
67
|
})();
|
|
52
68
|
const BOSUN_ATTACHED_PR_LABEL = "bosun-attached";
|
|
53
69
|
|
|
70
|
+
const HTML_TEXT_BREAK_TAGS = new Set([
|
|
71
|
+
"address",
|
|
72
|
+
"article",
|
|
73
|
+
"aside",
|
|
74
|
+
"blockquote",
|
|
75
|
+
"br",
|
|
76
|
+
"dd",
|
|
77
|
+
"div",
|
|
78
|
+
"dl",
|
|
79
|
+
"dt",
|
|
80
|
+
"figcaption",
|
|
81
|
+
"figure",
|
|
82
|
+
"footer",
|
|
83
|
+
"form",
|
|
84
|
+
"h1",
|
|
85
|
+
"h2",
|
|
86
|
+
"h3",
|
|
87
|
+
"h4",
|
|
88
|
+
"h5",
|
|
89
|
+
"h6",
|
|
90
|
+
"header",
|
|
91
|
+
"hr",
|
|
92
|
+
"li",
|
|
93
|
+
"main",
|
|
94
|
+
"nav",
|
|
95
|
+
"ol",
|
|
96
|
+
"p",
|
|
97
|
+
"pre",
|
|
98
|
+
"section",
|
|
99
|
+
"table",
|
|
100
|
+
"tbody",
|
|
101
|
+
"td",
|
|
102
|
+
"tfoot",
|
|
103
|
+
"th",
|
|
104
|
+
"thead",
|
|
105
|
+
"tr",
|
|
106
|
+
"ul",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
function decodeHtmlEntities(value = "") {
|
|
110
|
+
return String(value).replace(/&(?:nbsp|amp|lt|gt|quot|apos|#39|#\d+|#x[0-9a-f]+);/gi, (entity) => {
|
|
111
|
+
const normalized = entity.toLowerCase();
|
|
112
|
+
switch (normalized) {
|
|
113
|
+
case " ":
|
|
114
|
+
return " ";
|
|
115
|
+
case "&":
|
|
116
|
+
return "&";
|
|
117
|
+
case "<":
|
|
118
|
+
return "<";
|
|
119
|
+
case ">":
|
|
120
|
+
return ">";
|
|
121
|
+
case """:
|
|
122
|
+
return '"';
|
|
123
|
+
case "'":
|
|
124
|
+
case "'":
|
|
125
|
+
return "'";
|
|
126
|
+
default:
|
|
127
|
+
if (normalized.startsWith("&#x")) {
|
|
128
|
+
return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16));
|
|
129
|
+
}
|
|
130
|
+
if (normalized.startsWith("&#")) {
|
|
131
|
+
return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10));
|
|
132
|
+
}
|
|
133
|
+
return entity;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function stripHtmlToText(html = "") {
|
|
139
|
+
const input = String(html ?? "");
|
|
140
|
+
let plain = "";
|
|
141
|
+
let index = 0;
|
|
142
|
+
let skippedTagName = null;
|
|
143
|
+
|
|
144
|
+
while (index < input.length) {
|
|
145
|
+
const tagStart = input.indexOf("<", index);
|
|
146
|
+
if (tagStart === -1) {
|
|
147
|
+
if (!skippedTagName) plain += input.slice(index);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!skippedTagName && tagStart > index) {
|
|
152
|
+
plain += input.slice(index, tagStart);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const tagEnd = input.indexOf(">", tagStart + 1);
|
|
156
|
+
if (tagEnd === -1) {
|
|
157
|
+
if (!skippedTagName) plain += input.slice(tagStart).replace(/</g, " ");
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rawTag = input.slice(tagStart + 1, tagEnd).trim();
|
|
162
|
+
const loweredTag = rawTag.toLowerCase();
|
|
163
|
+
const isClosingTag = loweredTag.startsWith("/");
|
|
164
|
+
const normalizedTag = isClosingTag ? loweredTag.slice(1).trimStart() : loweredTag;
|
|
165
|
+
const tagName = normalizedTag.match(/^[a-z0-9]+/i)?.[0] ?? "";
|
|
166
|
+
|
|
167
|
+
if (skippedTagName) {
|
|
168
|
+
if (isClosingTag && tagName === skippedTagName) {
|
|
169
|
+
skippedTagName = null;
|
|
170
|
+
plain += " ";
|
|
171
|
+
}
|
|
172
|
+
index = tagEnd + 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (tagName === "script" || tagName === "style") {
|
|
177
|
+
if (!isClosingTag && !normalizedTag.endsWith("/")) {
|
|
178
|
+
skippedTagName = tagName;
|
|
179
|
+
}
|
|
180
|
+
index = tagEnd + 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (HTML_TEXT_BREAK_TAGS.has(tagName)) {
|
|
185
|
+
plain += " ";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
index = tagEnd + 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return decodeHtmlEntities(plain);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
const PORT_TYPE_DESCRIPTIONS = Object.freeze({
|
|
196
|
+
Any: "Wildcard payload",
|
|
197
|
+
TaskDef: "Task definition/context payload",
|
|
198
|
+
TriggerEvent: "Event payload emitted by trigger nodes",
|
|
199
|
+
AgentResult: "Agent execution output",
|
|
200
|
+
String: "Text payload",
|
|
201
|
+
Boolean: "Boolean flag",
|
|
202
|
+
Number: "Numeric payload",
|
|
203
|
+
JSON: "Structured JSON payload",
|
|
204
|
+
GitRef: "Git branch/hash/ref payload",
|
|
205
|
+
PRUrl: "Pull request URL payload",
|
|
206
|
+
LogStream: "Log output or command transcript",
|
|
207
|
+
SessionRef: "Session identifier payload",
|
|
208
|
+
CommandResult: "Command execution result",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const PORT_TYPE_COLORS = Object.freeze({
|
|
212
|
+
Any: "#9ca3af",
|
|
213
|
+
TaskDef: "#10b981",
|
|
214
|
+
TriggerEvent: "#22c55e",
|
|
215
|
+
AgentResult: "#8b5cf6",
|
|
216
|
+
String: "#3b82f6",
|
|
217
|
+
Boolean: "#14b8a6",
|
|
218
|
+
Number: "#0ea5e9",
|
|
219
|
+
JSON: "#06b6d4",
|
|
220
|
+
GitRef: "#f97316",
|
|
221
|
+
PRUrl: "#f43f5e",
|
|
222
|
+
LogStream: "#eab308",
|
|
223
|
+
SessionRef: "#a855f7",
|
|
224
|
+
CommandResult: "#f59e0b",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
function clonePortSpec(port, fallbackName = "default") {
|
|
228
|
+
if (!port || typeof port !== "object") {
|
|
229
|
+
const type = "Any";
|
|
230
|
+
return {
|
|
231
|
+
name: fallbackName,
|
|
232
|
+
label: fallbackName,
|
|
233
|
+
type,
|
|
234
|
+
description: PORT_TYPE_DESCRIPTIONS[type],
|
|
235
|
+
color: PORT_TYPE_COLORS[type] || null,
|
|
236
|
+
accepts: [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const type = String(port.type || "Any").trim() || "Any";
|
|
240
|
+
return {
|
|
241
|
+
...port,
|
|
242
|
+
name: String(port.name || fallbackName).trim() || fallbackName,
|
|
243
|
+
label: String(port.label || port.name || fallbackName).trim() || fallbackName,
|
|
244
|
+
type,
|
|
245
|
+
description: String(port.description || PORT_TYPE_DESCRIPTIONS[type] || "").trim(),
|
|
246
|
+
color: String(port.color || PORT_TYPE_COLORS[type] || "").trim() || null,
|
|
247
|
+
accepts: Array.isArray(port.accepts)
|
|
248
|
+
? Array.from(new Set(port.accepts.map((value) => String(value || "").trim()).filter(Boolean)))
|
|
249
|
+
: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function makePort(name, type, description = "", extra = {}) {
|
|
254
|
+
return clonePortSpec({
|
|
255
|
+
name,
|
|
256
|
+
label: name,
|
|
257
|
+
type,
|
|
258
|
+
description: description || PORT_TYPE_DESCRIPTIONS[type] || "",
|
|
259
|
+
color: PORT_TYPE_COLORS[type] || null,
|
|
260
|
+
...extra,
|
|
261
|
+
}, name || "default");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const CATEGORY_PORT_DEFAULTS = Object.freeze({
|
|
265
|
+
trigger: Object.freeze({
|
|
266
|
+
inputs: [],
|
|
267
|
+
outputs: [makePort("default", "TriggerEvent")],
|
|
268
|
+
}),
|
|
269
|
+
condition: Object.freeze({
|
|
270
|
+
inputs: [makePort("default", "JSON", "", { accepts: ["TriggerEvent", "TaskDef", "AgentResult", "String", "Any"] })],
|
|
271
|
+
outputs: [makePort("default", "Boolean")],
|
|
272
|
+
}),
|
|
273
|
+
action: Object.freeze({
|
|
274
|
+
inputs: [makePort("default", "TaskDef", "", { accepts: ["TriggerEvent", "JSON", "String", "Boolean", "Any"] })],
|
|
275
|
+
outputs: [makePort("default", "JSON")],
|
|
276
|
+
}),
|
|
277
|
+
validation: Object.freeze({
|
|
278
|
+
inputs: [makePort("default", "JSON", "", { accepts: ["TaskDef", "Any"] })],
|
|
279
|
+
outputs: [makePort("default", "Boolean")],
|
|
280
|
+
}),
|
|
281
|
+
transform: Object.freeze({
|
|
282
|
+
inputs: [makePort("default", "JSON", "", { accepts: ["Any", "String"] })],
|
|
283
|
+
outputs: [makePort("default", "JSON")],
|
|
284
|
+
}),
|
|
285
|
+
notify: Object.freeze({
|
|
286
|
+
inputs: [makePort("default", "String", "", { accepts: ["Any", "JSON", "AgentResult", "LogStream"] })],
|
|
287
|
+
outputs: [makePort("default", "Any")],
|
|
288
|
+
}),
|
|
289
|
+
flow: Object.freeze({
|
|
290
|
+
inputs: [makePort("default", "Any")],
|
|
291
|
+
outputs: [makePort("default", "Any")],
|
|
292
|
+
}),
|
|
293
|
+
loop: Object.freeze({
|
|
294
|
+
inputs: [makePort("default", "Any")],
|
|
295
|
+
outputs: [makePort("default", "Any")],
|
|
296
|
+
}),
|
|
297
|
+
meeting: Object.freeze({
|
|
298
|
+
inputs: [makePort("default", "SessionRef", "", { accepts: ["TriggerEvent", "Any"] })],
|
|
299
|
+
outputs: [makePort("default", "JSON")],
|
|
300
|
+
}),
|
|
301
|
+
agent: Object.freeze({
|
|
302
|
+
inputs: [makePort("default", "TaskDef", "", { accepts: ["TriggerEvent", "JSON", "String", "Any"] })],
|
|
303
|
+
outputs: [makePort("default", "AgentResult")],
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const NODE_PORT_OVERRIDES = Object.freeze({
|
|
308
|
+
"trigger.manual": {
|
|
309
|
+
outputs: [makePort("default", "TaskDef", "Manual dispatch payload")],
|
|
310
|
+
},
|
|
311
|
+
"trigger.event": {
|
|
312
|
+
outputs: [makePort("default", "TriggerEvent", "Event payload")],
|
|
313
|
+
},
|
|
314
|
+
"action.run_agent": {
|
|
315
|
+
inputs: [makePort("default", "TaskDef", "", { accepts: ["TriggerEvent", "String", "JSON", "Boolean", "Any"] })],
|
|
316
|
+
outputs: [makePort("default", "AgentResult", "Agent response payload")],
|
|
317
|
+
},
|
|
318
|
+
"action.run_command": {
|
|
319
|
+
inputs: [makePort("default", "String", "", { accepts: ["TaskDef", "JSON", "TriggerEvent", "Boolean", "Any"] })],
|
|
320
|
+
outputs: [makePort("default", "CommandResult", "Command execution output", { accepts: ["LogStream"] })],
|
|
321
|
+
},
|
|
322
|
+
"action.git_operations": {
|
|
323
|
+
inputs: [makePort("default", "GitRef", "", { accepts: ["TaskDef", "JSON", "TriggerEvent", "Boolean", "String", "Any"] })],
|
|
324
|
+
outputs: [makePort("default", "GitRef", "Git operation result/ref")],
|
|
325
|
+
},
|
|
326
|
+
"action.push_branch": {
|
|
327
|
+
inputs: [makePort("default", "GitRef", "", { accepts: ["TaskDef", "JSON", "TriggerEvent", "Boolean", "String", "Any"] })],
|
|
328
|
+
outputs: [makePort("default", "GitRef")],
|
|
329
|
+
},
|
|
330
|
+
"action.detect_new_commits": {
|
|
331
|
+
inputs: [makePort("default", "GitRef", "", { accepts: ["TaskDef", "JSON", "TriggerEvent", "Boolean", "String", "Any"] })],
|
|
332
|
+
outputs: [makePort("default", "GitRef", "Commit detection summary")],
|
|
333
|
+
},
|
|
334
|
+
"action.create_pr": {
|
|
335
|
+
inputs: [makePort("default", "GitRef", "", { accepts: ["TaskDef", "JSON", "TriggerEvent", "Boolean", "String", "Any"] })],
|
|
336
|
+
outputs: [makePort("default", "PRUrl", "Pull request link payload")],
|
|
337
|
+
},
|
|
338
|
+
"transform.json_parse": {
|
|
339
|
+
inputs: [makePort("default", "String", "", { accepts: ["JSON", "Any"] })],
|
|
340
|
+
outputs: [makePort("default", "JSON")],
|
|
341
|
+
},
|
|
342
|
+
"condition.expression": {
|
|
343
|
+
inputs: [makePort("default", "JSON", "", { accepts: ["TaskDef", "AgentResult", "Any"] })],
|
|
344
|
+
outputs: [makePort("default", "Boolean")],
|
|
345
|
+
},
|
|
346
|
+
"notify.log": {
|
|
347
|
+
inputs: [makePort("default", "LogStream", "", { accepts: ["String", "Any", "JSON"] })],
|
|
348
|
+
outputs: [makePort("default", "LogStream")],
|
|
349
|
+
},
|
|
350
|
+
"action.continue_session": {
|
|
351
|
+
inputs: [makePort("default", "SessionRef", "", { accepts: ["TaskDef", "Any"] })],
|
|
352
|
+
outputs: [makePort("default", "AgentResult")],
|
|
353
|
+
},
|
|
354
|
+
"action.restart_agent": {
|
|
355
|
+
inputs: [makePort("default", "SessionRef", "", { accepts: ["TaskDef", "Any"] })],
|
|
356
|
+
outputs: [makePort("default", "AgentResult")],
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const NODE_PRIMARY_FIELD_OVERRIDES = Object.freeze({
|
|
361
|
+
"action.run_agent": ["model", "prompt", "stream"],
|
|
362
|
+
"condition.expression": ["expression"],
|
|
363
|
+
"trigger.event": ["eventType", "filter"],
|
|
364
|
+
"trigger.schedule": ["intervalMs", "cron"],
|
|
365
|
+
"trigger.scheduled_once": ["runAt", "timezone"],
|
|
366
|
+
"action.git_operations": ["operation", "branch", "targetBranch"],
|
|
367
|
+
"action.create_pr": ["title", "baseBranch", "headBranch"],
|
|
368
|
+
"action.run_command": ["command", "cwd"],
|
|
369
|
+
"notify.telegram": ["chatId", "message"],
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
function inferPrimaryFields(schemaProps = {}) {
|
|
373
|
+
const keys = Object.keys(schemaProps || {});
|
|
374
|
+
if (keys.length === 0) return [];
|
|
375
|
+
const priority = [
|
|
376
|
+
"model",
|
|
377
|
+
"expression",
|
|
378
|
+
"enabled",
|
|
379
|
+
"branch",
|
|
380
|
+
"branchName",
|
|
381
|
+
"baseBranch",
|
|
382
|
+
"headBranch",
|
|
383
|
+
"eventType",
|
|
384
|
+
"command",
|
|
385
|
+
"message",
|
|
386
|
+
"prompt",
|
|
387
|
+
"query",
|
|
388
|
+
"operation",
|
|
389
|
+
"timeout",
|
|
390
|
+
];
|
|
391
|
+
const selected = [];
|
|
392
|
+
for (const key of priority) {
|
|
393
|
+
if (keys.includes(key) && !selected.includes(key)) selected.push(key);
|
|
394
|
+
if (selected.length >= 3) return selected;
|
|
395
|
+
}
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
const field = schemaProps[key] || {};
|
|
398
|
+
const type = String(field.type || "string");
|
|
399
|
+
const isShortString = type === "string" && !field.format && !String(key).toLowerCase().includes("path");
|
|
400
|
+
const isBoolean = type === "boolean";
|
|
401
|
+
if (field.enum || isShortString || isBoolean) {
|
|
402
|
+
if (!selected.includes(key)) selected.push(key);
|
|
403
|
+
}
|
|
404
|
+
if (selected.length >= 3) break;
|
|
405
|
+
}
|
|
406
|
+
return selected.slice(0, 3);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildNodePorts(type, handler) {
|
|
410
|
+
const explicitPorts = handler?.ports || {};
|
|
411
|
+
const explicitInputs = Array.isArray(handler?.inputs) ? handler.inputs : explicitPorts.inputs;
|
|
412
|
+
const explicitOutputs = Array.isArray(handler?.outputs) ? handler.outputs : explicitPorts.outputs;
|
|
413
|
+
if (Array.isArray(explicitInputs) || Array.isArray(explicitOutputs)) {
|
|
414
|
+
return {
|
|
415
|
+
inputs: (explicitInputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `input-${index + 1}`)),
|
|
416
|
+
outputs: (explicitOutputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `output-${index + 1}`)),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const override = NODE_PORT_OVERRIDES[type];
|
|
421
|
+
if (override) {
|
|
422
|
+
return {
|
|
423
|
+
inputs: (override.inputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `input-${index + 1}`)),
|
|
424
|
+
outputs: (override.outputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `output-${index + 1}`)),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const [category] = String(type || "").split(".");
|
|
429
|
+
const fallback = CATEGORY_PORT_DEFAULTS[category] || CATEGORY_PORT_DEFAULTS.flow;
|
|
430
|
+
return {
|
|
431
|
+
inputs: (fallback.inputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `input-${index + 1}`)),
|
|
432
|
+
outputs: (fallback.outputs || []).map((port, index) => clonePortSpec(port, index === 0 ? "default" : `output-${index + 1}`)),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function buildNodeUi(type, handler) {
|
|
437
|
+
const schemaProps = handler?.schema?.properties || {};
|
|
438
|
+
const explicitPrimaryFields = Array.isArray(handler?.ui?.primaryFields)
|
|
439
|
+
? handler.ui.primaryFields
|
|
440
|
+
: null;
|
|
441
|
+
const inferred = NODE_PRIMARY_FIELD_OVERRIDES[type] || inferPrimaryFields(schemaProps);
|
|
442
|
+
return {
|
|
443
|
+
...(handler?.ui || {}),
|
|
444
|
+
primaryFields: (explicitPrimaryFields || inferred)
|
|
445
|
+
.map((value) => String(value || "").trim())
|
|
446
|
+
.filter(Boolean),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function registerBuiltinNodeType(type, handler) {
|
|
451
|
+
const ports = buildNodePorts(type, handler);
|
|
452
|
+
const ui = buildNodeUi(type, handler);
|
|
453
|
+
handler.ports = ports;
|
|
454
|
+
handler.ui = ui;
|
|
455
|
+
registerNodeType(type, handler);
|
|
456
|
+
}
|
|
457
|
+
|
|
54
458
|
function shouldBypassGhPrCreationForTests() {
|
|
55
459
|
return Boolean(process.env.VITEST) && process.env.BOSUN_TEST_ALLOW_GH !== "true";
|
|
56
460
|
}
|
|
@@ -885,7 +1289,25 @@ function normalizeLegacyWorkflowCommand(command) {
|
|
|
885
1289
|
}
|
|
886
1290
|
|
|
887
1291
|
function resolveWorkflowNodeValue(value, ctx) {
|
|
888
|
-
if (typeof value === "string")
|
|
1292
|
+
if (typeof value === "string") {
|
|
1293
|
+
const resolved = ctx.resolve(value);
|
|
1294
|
+
if (resolved !== value) return resolved;
|
|
1295
|
+
|
|
1296
|
+
const exactExpr = value.match(/^\{\{([\s\S]+)\}\}$/);
|
|
1297
|
+
if (exactExpr) {
|
|
1298
|
+
const expr = String(exactExpr[1] || "").trim();
|
|
1299
|
+
if (expr.includes("$ctx") || expr.includes("$data") || expr.includes("$output")) {
|
|
1300
|
+
try {
|
|
1301
|
+
const fn = new Function("$data", "$ctx", "$output", `return (${expr});`);
|
|
1302
|
+
const evalResult = fn(ctx.data || {}, ctx, null);
|
|
1303
|
+
if (evalResult !== undefined) return evalResult;
|
|
1304
|
+
} catch {
|
|
1305
|
+
// Fall through to unresolved template string when expression is invalid.
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return resolved;
|
|
1310
|
+
}
|
|
889
1311
|
if (Array.isArray(value)) {
|
|
890
1312
|
return value.map((item) => resolveWorkflowNodeValue(item, ctx));
|
|
891
1313
|
}
|
|
@@ -1152,7 +1574,7 @@ function buildWorkflowAgentToolContract(rootDir, agentProfileId = "") {
|
|
|
1152
1574
|
// TRIGGERS — Events that initiate a workflow
|
|
1153
1575
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1154
1576
|
|
|
1155
|
-
|
|
1577
|
+
registerBuiltinNodeType("trigger.manual", {
|
|
1156
1578
|
describe: () => "Manual trigger — workflow starts on user request",
|
|
1157
1579
|
schema: {
|
|
1158
1580
|
type: "object",
|
|
@@ -1164,7 +1586,7 @@ registerNodeType("trigger.manual", {
|
|
|
1164
1586
|
},
|
|
1165
1587
|
});
|
|
1166
1588
|
|
|
1167
|
-
|
|
1589
|
+
registerBuiltinNodeType("trigger.task_low", {
|
|
1168
1590
|
describe: () =>
|
|
1169
1591
|
"Fires when backlog task count drops below threshold. Self-queries kanban " +
|
|
1170
1592
|
"when todoCount is not pre-populated in context data. Workspace-aware: " +
|
|
@@ -1230,7 +1652,7 @@ registerNodeType("trigger.task_low", {
|
|
|
1230
1652
|
},
|
|
1231
1653
|
});
|
|
1232
1654
|
|
|
1233
|
-
|
|
1655
|
+
registerBuiltinNodeType("trigger.schedule", {
|
|
1234
1656
|
describe: () => "Fires on a cron-like schedule (checked by supervisor loop)",
|
|
1235
1657
|
schema: {
|
|
1236
1658
|
type: "object",
|
|
@@ -1249,7 +1671,7 @@ registerNodeType("trigger.schedule", {
|
|
|
1249
1671
|
},
|
|
1250
1672
|
});
|
|
1251
1673
|
|
|
1252
|
-
|
|
1674
|
+
registerBuiltinNodeType("trigger.event", {
|
|
1253
1675
|
describe: () => "Fires on a specific bosun event (task.complete, pr.merged, etc.)",
|
|
1254
1676
|
schema: {
|
|
1255
1677
|
type: "object",
|
|
@@ -1274,7 +1696,7 @@ registerNodeType("trigger.event", {
|
|
|
1274
1696
|
},
|
|
1275
1697
|
});
|
|
1276
1698
|
|
|
1277
|
-
|
|
1699
|
+
registerBuiltinNodeType("trigger.meeting.wake_phrase", {
|
|
1278
1700
|
describe: () => "Fires when a transcript/event payload contains the configured wake phrase",
|
|
1279
1701
|
schema: {
|
|
1280
1702
|
type: "object",
|
|
@@ -1433,7 +1855,7 @@ registerNodeType("trigger.meeting.wake_phrase", {
|
|
|
1433
1855
|
},
|
|
1434
1856
|
});
|
|
1435
1857
|
|
|
1436
|
-
|
|
1858
|
+
registerBuiltinNodeType("trigger.webhook", {
|
|
1437
1859
|
describe: () => "Fires when a webhook is received at the workflow's endpoint",
|
|
1438
1860
|
schema: {
|
|
1439
1861
|
type: "object",
|
|
@@ -1447,7 +1869,7 @@ registerNodeType("trigger.webhook", {
|
|
|
1447
1869
|
},
|
|
1448
1870
|
});
|
|
1449
1871
|
|
|
1450
|
-
|
|
1872
|
+
registerBuiltinNodeType("trigger.pr_event", {
|
|
1451
1873
|
describe: () => "Fires on PR events (opened, merged, review requested, etc.)",
|
|
1452
1874
|
schema: {
|
|
1453
1875
|
type: "object",
|
|
@@ -1484,7 +1906,7 @@ registerNodeType("trigger.pr_event", {
|
|
|
1484
1906
|
},
|
|
1485
1907
|
});
|
|
1486
1908
|
|
|
1487
|
-
|
|
1909
|
+
registerBuiltinNodeType("trigger.task_assigned", {
|
|
1488
1910
|
describe: () => "Fires when a task is assigned to an agent",
|
|
1489
1911
|
schema: {
|
|
1490
1912
|
type: "object",
|
|
@@ -1500,7 +1922,7 @@ registerNodeType("trigger.task_assigned", {
|
|
|
1500
1922
|
},
|
|
1501
1923
|
});
|
|
1502
1924
|
|
|
1503
|
-
|
|
1925
|
+
registerBuiltinNodeType("trigger.anomaly", {
|
|
1504
1926
|
describe: () => "Fires when the anomaly detector reports an anomaly matching the configured criteria",
|
|
1505
1927
|
schema: {
|
|
1506
1928
|
type: "object",
|
|
@@ -1540,7 +1962,7 @@ registerNodeType("trigger.anomaly", {
|
|
|
1540
1962
|
},
|
|
1541
1963
|
});
|
|
1542
1964
|
|
|
1543
|
-
|
|
1965
|
+
registerBuiltinNodeType("trigger.scheduled_once", {
|
|
1544
1966
|
describe: () => "Fires once at or after a specific scheduled time (persistent — survives restarts)",
|
|
1545
1967
|
schema: {
|
|
1546
1968
|
type: "object",
|
|
@@ -1577,7 +1999,7 @@ registerNodeType("trigger.scheduled_once", {
|
|
|
1577
1999
|
},
|
|
1578
2000
|
});
|
|
1579
2001
|
|
|
1580
|
-
|
|
2002
|
+
registerBuiltinNodeType("trigger.workflow_call", {
|
|
1581
2003
|
describe: () =>
|
|
1582
2004
|
"Fires when this workflow is invoked by another workflow via action.execute_workflow. " +
|
|
1583
2005
|
"Defines expected input parameters that callers should provide.",
|
|
@@ -1642,7 +2064,7 @@ registerNodeType("trigger.workflow_call", {
|
|
|
1642
2064
|
// CONDITIONS — Branching / routing logic
|
|
1643
2065
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1644
2066
|
|
|
1645
|
-
|
|
2067
|
+
registerBuiltinNodeType("condition.expression", {
|
|
1646
2068
|
describe: () => "Evaluate a JS expression to branch workflow execution",
|
|
1647
2069
|
schema: {
|
|
1648
2070
|
type: "object",
|
|
@@ -1667,7 +2089,7 @@ registerNodeType("condition.expression", {
|
|
|
1667
2089
|
},
|
|
1668
2090
|
});
|
|
1669
2091
|
|
|
1670
|
-
|
|
2092
|
+
registerBuiltinNodeType("condition.task_has_tag", {
|
|
1671
2093
|
describe: () => "Check if current task has a specific tag or label",
|
|
1672
2094
|
schema: {
|
|
1673
2095
|
type: "object",
|
|
@@ -1689,7 +2111,7 @@ registerNodeType("condition.task_has_tag", {
|
|
|
1689
2111
|
},
|
|
1690
2112
|
});
|
|
1691
2113
|
|
|
1692
|
-
|
|
2114
|
+
registerBuiltinNodeType("condition.file_exists", {
|
|
1693
2115
|
describe: () => "Check if a file or directory exists in the workspace",
|
|
1694
2116
|
schema: {
|
|
1695
2117
|
type: "object",
|
|
@@ -1706,7 +2128,7 @@ registerNodeType("condition.file_exists", {
|
|
|
1706
2128
|
},
|
|
1707
2129
|
});
|
|
1708
2130
|
|
|
1709
|
-
|
|
2131
|
+
registerBuiltinNodeType("condition.switch", {
|
|
1710
2132
|
describe: () => "Multi-way branch based on a value matching cases",
|
|
1711
2133
|
schema: {
|
|
1712
2134
|
type: "object",
|
|
@@ -1759,12 +2181,13 @@ registerNodeType("condition.switch", {
|
|
|
1759
2181
|
// ACTIONS — Side-effect operations
|
|
1760
2182
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1761
2183
|
|
|
1762
|
-
|
|
2184
|
+
registerBuiltinNodeType("action.run_agent", {
|
|
1763
2185
|
describe: () => "Run a bosun agent with a prompt to perform work",
|
|
1764
2186
|
schema: {
|
|
1765
2187
|
type: "object",
|
|
1766
2188
|
properties: {
|
|
1767
2189
|
prompt: { type: "string", description: "Agent prompt (supports {{variables}})" },
|
|
2190
|
+
systemPrompt: { type: "string", description: "Optional stable system prompt for cache anchoring" },
|
|
1768
2191
|
sdk: { type: "string", enum: ["codex", "copilot", "claude", "auto"], default: "auto" },
|
|
1769
2192
|
model: { type: "string", description: "Optional model override for the selected SDK" },
|
|
1770
2193
|
taskId: { type: "string", description: "Optional task ID used for task metadata lookup" },
|
|
@@ -1822,8 +2245,16 @@ registerNodeType("action.run_agent", {
|
|
|
1822
2245
|
? Math.max(1000, Math.trunc(Number(resolvedTimeoutMs)))
|
|
1823
2246
|
: 3600000;
|
|
1824
2247
|
const includeTaskContext = node.config?.includeTaskContext !== false;
|
|
2248
|
+
const configuredSystemPrompt =
|
|
2249
|
+
ctx.resolve(node.config?.systemPrompt || "") ||
|
|
2250
|
+
ctx.data?._taskSystemPrompt ||
|
|
2251
|
+
"";
|
|
1825
2252
|
const toolContract = buildWorkflowAgentToolContract(cwd, agentProfileId);
|
|
1826
|
-
|
|
2253
|
+
const effectiveSystemPrompt = [configuredSystemPrompt, toolContract]
|
|
2254
|
+
.map((value) => String(value || "").trim())
|
|
2255
|
+
.filter(Boolean)
|
|
2256
|
+
.join("\n\n");
|
|
2257
|
+
let finalPrompt = prompt;
|
|
1827
2258
|
if (includeTaskContext) {
|
|
1828
2259
|
const explicitContext =
|
|
1829
2260
|
ctx.data?.taskContext ||
|
|
@@ -1831,7 +2262,7 @@ registerNodeType("action.run_agent", {
|
|
|
1831
2262
|
null;
|
|
1832
2263
|
const task = ctx.data?.task || ctx.data?.taskDetail || ctx.data?.taskInfo || null;
|
|
1833
2264
|
const contextBlock = explicitContext || buildTaskContextBlock(task);
|
|
1834
|
-
if (contextBlock) finalPrompt = `${
|
|
2265
|
+
if (contextBlock) finalPrompt = `${finalPrompt}\n\n${contextBlock}`;
|
|
1835
2266
|
}
|
|
1836
2267
|
|
|
1837
2268
|
ctx.log(node.id, `Running agent (${sdk}) in ${cwd}`);
|
|
@@ -2167,6 +2598,7 @@ registerNodeType("action.run_agent", {
|
|
|
2167
2598
|
sdk: sdkOverride,
|
|
2168
2599
|
model: modelOverride,
|
|
2169
2600
|
onEvent: launchExtra.onEvent,
|
|
2601
|
+
systemPrompt: effectiveSystemPrompt,
|
|
2170
2602
|
});
|
|
2171
2603
|
}
|
|
2172
2604
|
|
|
@@ -2178,10 +2610,12 @@ registerNodeType("action.run_agent", {
|
|
|
2178
2610
|
sdk: sdkOverride,
|
|
2179
2611
|
model: modelOverride,
|
|
2180
2612
|
onEvent: launchExtra.onEvent,
|
|
2613
|
+
systemPrompt: effectiveSystemPrompt,
|
|
2181
2614
|
});
|
|
2182
2615
|
}
|
|
2183
2616
|
|
|
2184
2617
|
if (!result) {
|
|
2618
|
+
launchExtra.systemPrompt = effectiveSystemPrompt;
|
|
2185
2619
|
result = await agentPool.launchEphemeralThread(passPrompt, cwd, timeoutMs, launchExtra);
|
|
2186
2620
|
}
|
|
2187
2621
|
success = result?.success === true;
|
|
@@ -2477,13 +2911,14 @@ registerNodeType("action.run_agent", {
|
|
|
2477
2911
|
},
|
|
2478
2912
|
});
|
|
2479
2913
|
|
|
2480
|
-
|
|
2914
|
+
registerBuiltinNodeType("action.run_command", {
|
|
2481
2915
|
describe: () => "Execute a shell command in the workspace",
|
|
2482
2916
|
schema: {
|
|
2483
2917
|
type: "object",
|
|
2484
2918
|
properties: {
|
|
2485
2919
|
command: { type: "string", description: "Shell command to run" },
|
|
2486
2920
|
cwd: { type: "string", description: "Working directory" },
|
|
2921
|
+
env: { type: "object", description: "Environment variables passed to the command (supports templates)", additionalProperties: true },
|
|
2487
2922
|
timeoutMs: { type: "number", default: 300000 },
|
|
2488
2923
|
shell: { type: "string", default: "auto", enum: ["auto", "bash", "pwsh", "cmd"] },
|
|
2489
2924
|
captureOutput: { type: "boolean", default: true },
|
|
@@ -2495,6 +2930,20 @@ registerNodeType("action.run_command", {
|
|
|
2495
2930
|
const resolvedCommand = ctx.resolve(node.config?.command || "");
|
|
2496
2931
|
const command = normalizeLegacyWorkflowCommand(resolvedCommand);
|
|
2497
2932
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
2933
|
+
const resolvedEnvConfig = resolveWorkflowNodeValue(node.config?.env ?? {}, ctx);
|
|
2934
|
+
const commandEnv = { ...process.env };
|
|
2935
|
+
if (resolvedEnvConfig && typeof resolvedEnvConfig === "object" && !Array.isArray(resolvedEnvConfig)) {
|
|
2936
|
+
for (const [key, value] of Object.entries(resolvedEnvConfig)) {
|
|
2937
|
+
const name = String(key || "").trim();
|
|
2938
|
+
if (!name) continue;
|
|
2939
|
+
if (value == null) {
|
|
2940
|
+
delete commandEnv[name];
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
commandEnv[name] = typeof value === "string" ? value : JSON.stringify(value);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2498
2947
|
const timeout = node.config?.timeoutMs || 300000;
|
|
2499
2948
|
|
|
2500
2949
|
if (command !== resolvedCommand) {
|
|
@@ -2508,6 +2957,7 @@ registerNodeType("action.run_command", {
|
|
|
2508
2957
|
encoding: "utf8",
|
|
2509
2958
|
maxBuffer: 10 * 1024 * 1024,
|
|
2510
2959
|
stdio: node.config?.captureOutput !== false ? "pipe" : "inherit",
|
|
2960
|
+
env: commandEnv,
|
|
2511
2961
|
});
|
|
2512
2962
|
ctx.log(node.id, `Command succeeded`);
|
|
2513
2963
|
return { success: true, output: output?.trim(), exitCode: 0 };
|
|
@@ -2530,7 +2980,7 @@ registerNodeType("action.run_command", {
|
|
|
2530
2980
|
},
|
|
2531
2981
|
});
|
|
2532
2982
|
|
|
2533
|
-
|
|
2983
|
+
registerBuiltinNodeType("action.execute_workflow", {
|
|
2534
2984
|
describe: () => "Execute another workflow by ID (synchronously or dispatch mode)",
|
|
2535
2985
|
schema: {
|
|
2536
2986
|
type: "object",
|
|
@@ -2751,7 +3201,313 @@ registerNodeType("action.execute_workflow", {
|
|
|
2751
3201
|
},
|
|
2752
3202
|
});
|
|
2753
3203
|
|
|
2754
|
-
|
|
3204
|
+
registerBuiltinNodeType("action.inline_workflow", {
|
|
3205
|
+
describe: () =>
|
|
3206
|
+
"Execute an embedded workflow definition inline (sync or dispatch) without saving it. " +
|
|
3207
|
+
"Useful for parent workflows that need a local subgraph with its own run/context boundary.",
|
|
3208
|
+
schema: {
|
|
3209
|
+
type: "object",
|
|
3210
|
+
properties: {
|
|
3211
|
+
workflow: {
|
|
3212
|
+
type: "object",
|
|
3213
|
+
description:
|
|
3214
|
+
"Embedded workflow definition fragment. Supports { name, variables, nodes, edges, trigger, metadata }.",
|
|
3215
|
+
additionalProperties: true,
|
|
3216
|
+
},
|
|
3217
|
+
mode: { type: "string", enum: ["sync", "dispatch"], default: "sync" },
|
|
3218
|
+
input: {
|
|
3219
|
+
type: "object",
|
|
3220
|
+
description: "Input payload passed to the embedded workflow",
|
|
3221
|
+
additionalProperties: true,
|
|
3222
|
+
},
|
|
3223
|
+
inheritContext: {
|
|
3224
|
+
type: "boolean",
|
|
3225
|
+
default: false,
|
|
3226
|
+
description: "Copy parent workflow context data into child input before applying input overrides",
|
|
3227
|
+
},
|
|
3228
|
+
includeKeys: {
|
|
3229
|
+
type: "array",
|
|
3230
|
+
items: { type: "string" },
|
|
3231
|
+
description: "Optional allow-list of parent context keys to inherit when inheritContext=true",
|
|
3232
|
+
},
|
|
3233
|
+
outputVariable: {
|
|
3234
|
+
type: "string",
|
|
3235
|
+
description: "Optional context key to store execution summary output",
|
|
3236
|
+
},
|
|
3237
|
+
failOnChildError: {
|
|
3238
|
+
type: "boolean",
|
|
3239
|
+
default: true,
|
|
3240
|
+
description: "In sync mode, throw when the embedded workflow completes with errors",
|
|
3241
|
+
},
|
|
3242
|
+
forwardFields: {
|
|
3243
|
+
type: "array",
|
|
3244
|
+
items: { type: "string" },
|
|
3245
|
+
description:
|
|
3246
|
+
"Optional allow-list of top-level fields from the embedded workflow's extracted outputs " +
|
|
3247
|
+
"to promote onto this node's output.",
|
|
3248
|
+
},
|
|
3249
|
+
extractFromNodes: {
|
|
3250
|
+
type: "array",
|
|
3251
|
+
items: { type: "string" },
|
|
3252
|
+
description:
|
|
3253
|
+
"Optional child node IDs to extract outputs from. When omitted, the last completed " +
|
|
3254
|
+
"child node output is used.",
|
|
3255
|
+
},
|
|
3256
|
+
allowRecursive: {
|
|
3257
|
+
type: "boolean",
|
|
3258
|
+
default: false,
|
|
3259
|
+
description: "Allow recursive embedded workflow execution when true",
|
|
3260
|
+
},
|
|
3261
|
+
},
|
|
3262
|
+
required: ["workflow"],
|
|
3263
|
+
},
|
|
3264
|
+
async execute(node, ctx, engine) {
|
|
3265
|
+
const workflowDef = resolveWorkflowNodeValue(node.config?.workflow ?? null, ctx);
|
|
3266
|
+
const modeRaw = String(ctx.resolve(node.config?.mode || "sync") || "sync")
|
|
3267
|
+
.trim()
|
|
3268
|
+
.toLowerCase();
|
|
3269
|
+
const mode = modeRaw || "sync";
|
|
3270
|
+
const outputVariable = String(ctx.resolve(node.config?.outputVariable || "") || "").trim();
|
|
3271
|
+
const inheritContext = parseBooleanSetting(
|
|
3272
|
+
resolveWorkflowNodeValue(node.config?.inheritContext ?? false, ctx),
|
|
3273
|
+
false,
|
|
3274
|
+
);
|
|
3275
|
+
const failOnChildError = parseBooleanSetting(
|
|
3276
|
+
resolveWorkflowNodeValue(node.config?.failOnChildError ?? true, ctx),
|
|
3277
|
+
true,
|
|
3278
|
+
);
|
|
3279
|
+
const allowRecursive = parseBooleanSetting(
|
|
3280
|
+
resolveWorkflowNodeValue(node.config?.allowRecursive ?? false, ctx),
|
|
3281
|
+
false,
|
|
3282
|
+
);
|
|
3283
|
+
const includeKeys = Array.isArray(node.config?.includeKeys)
|
|
3284
|
+
? node.config.includeKeys
|
|
3285
|
+
.map((value) => String(resolveWorkflowNodeValue(value, ctx) || "").trim())
|
|
3286
|
+
.filter(Boolean)
|
|
3287
|
+
: [];
|
|
3288
|
+
const forwardFields = Array.isArray(node.config?.forwardFields)
|
|
3289
|
+
? node.config.forwardFields
|
|
3290
|
+
.map((value) => String(resolveWorkflowNodeValue(value, ctx) || "").trim())
|
|
3291
|
+
.filter(Boolean)
|
|
3292
|
+
: [];
|
|
3293
|
+
const extractFromNodes = Array.isArray(node.config?.extractFromNodes)
|
|
3294
|
+
? node.config.extractFromNodes
|
|
3295
|
+
.map((value) => String(resolveWorkflowNodeValue(value, ctx) || "").trim())
|
|
3296
|
+
.filter(Boolean)
|
|
3297
|
+
: [];
|
|
3298
|
+
|
|
3299
|
+
if (!workflowDef || typeof workflowDef !== "object" || Array.isArray(workflowDef)) {
|
|
3300
|
+
throw new Error("action.inline_workflow: 'workflow' must resolve to an object");
|
|
3301
|
+
}
|
|
3302
|
+
if (mode !== "sync" && mode !== "dispatch") {
|
|
3303
|
+
throw new Error(`action.inline_workflow: invalid mode "${mode}". Expected "sync" or "dispatch".`);
|
|
3304
|
+
}
|
|
3305
|
+
if (!engine || typeof engine.executeDefinition !== "function") {
|
|
3306
|
+
throw new Error("action.inline_workflow: workflow engine is not available");
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
const resolvedInputConfig = resolveWorkflowNodeValue(node.config?.input ?? {}, ctx);
|
|
3310
|
+
if (
|
|
3311
|
+
resolvedInputConfig != null &&
|
|
3312
|
+
(typeof resolvedInputConfig !== "object" || Array.isArray(resolvedInputConfig))
|
|
3313
|
+
) {
|
|
3314
|
+
throw new Error("action.inline_workflow: 'input' must resolve to an object");
|
|
3315
|
+
}
|
|
3316
|
+
const configuredInput =
|
|
3317
|
+
resolvedInputConfig && typeof resolvedInputConfig === "object"
|
|
3318
|
+
? resolvedInputConfig
|
|
3319
|
+
: {};
|
|
3320
|
+
|
|
3321
|
+
const sourceData =
|
|
3322
|
+
ctx.data && typeof ctx.data === "object"
|
|
3323
|
+
? ctx.data
|
|
3324
|
+
: {};
|
|
3325
|
+
const inheritedInput = {};
|
|
3326
|
+
if (inheritContext) {
|
|
3327
|
+
if (includeKeys.length > 0) {
|
|
3328
|
+
for (const key of includeKeys) {
|
|
3329
|
+
if (Object.prototype.hasOwnProperty.call(sourceData, key)) {
|
|
3330
|
+
inheritedInput[key] = sourceData[key];
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
} else {
|
|
3334
|
+
Object.assign(inheritedInput, sourceData);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
const parentWorkflowId = String(ctx.data?._workflowId || "").trim() || "inline-parent";
|
|
3339
|
+
const workflowStack = normalizeWorkflowStack(ctx.data?._workflowStack);
|
|
3340
|
+
if (parentWorkflowId && workflowStack[workflowStack.length - 1] !== parentWorkflowId) {
|
|
3341
|
+
workflowStack.push(parentWorkflowId);
|
|
3342
|
+
}
|
|
3343
|
+
const inlineWorkflowId = String(workflowDef.id || `inline:${parentWorkflowId}:${node.id}`).trim();
|
|
3344
|
+
if (!allowRecursive && workflowStack.includes(inlineWorkflowId)) {
|
|
3345
|
+
const cyclePath = [...workflowStack, inlineWorkflowId].join(" -> ");
|
|
3346
|
+
throw new Error(
|
|
3347
|
+
`action.inline_workflow: recursive inline workflow call blocked (${cyclePath}). ` +
|
|
3348
|
+
"Set allowRecursive=true to override.",
|
|
3349
|
+
);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
const childInput = {
|
|
3353
|
+
...inheritedInput,
|
|
3354
|
+
...configuredInput,
|
|
3355
|
+
_workflowStack: [...workflowStack, inlineWorkflowId],
|
|
3356
|
+
_parentWorkflowId: parentWorkflowId,
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
const inlineName = String(workflowDef.name || node.label || `Inline ${node.id}`).trim() || inlineWorkflowId;
|
|
3360
|
+
const executeInline = () => engine.executeDefinition({
|
|
3361
|
+
trigger: workflowDef.trigger || "trigger.workflow_call",
|
|
3362
|
+
...workflowDef,
|
|
3363
|
+
id: inlineWorkflowId,
|
|
3364
|
+
name: inlineName,
|
|
3365
|
+
metadata: {
|
|
3366
|
+
...(workflowDef.metadata || {}),
|
|
3367
|
+
inline: true,
|
|
3368
|
+
sourceNodeId: node.id,
|
|
3369
|
+
parentWorkflowId,
|
|
3370
|
+
},
|
|
3371
|
+
}, childInput, {
|
|
3372
|
+
force: true,
|
|
3373
|
+
sourceNodeId: node.id,
|
|
3374
|
+
inlineWorkflowId,
|
|
3375
|
+
inlineWorkflowName: inlineName,
|
|
3376
|
+
});
|
|
3377
|
+
|
|
3378
|
+
const extractChildOutputs = (childCtx) => {
|
|
3379
|
+
const childOutputs = childCtx?.nodeOutputs instanceof Map
|
|
3380
|
+
? Object.fromEntries(childCtx.nodeOutputs)
|
|
3381
|
+
: childCtx?.nodeOutputs && typeof childCtx.nodeOutputs === "object"
|
|
3382
|
+
? { ...childCtx.nodeOutputs }
|
|
3383
|
+
: {};
|
|
3384
|
+
|
|
3385
|
+
let extracted = {};
|
|
3386
|
+
if (extractFromNodes.length > 0) {
|
|
3387
|
+
for (const childNodeId of extractFromNodes) {
|
|
3388
|
+
if (Object.prototype.hasOwnProperty.call(childOutputs, childNodeId)) {
|
|
3389
|
+
extracted[childNodeId] = childOutputs[childNodeId];
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
if (extractFromNodes.length === 1) {
|
|
3393
|
+
const single = extracted[extractFromNodes[0]];
|
|
3394
|
+
if (
|
|
3395
|
+
single &&
|
|
3396
|
+
typeof single === "object" &&
|
|
3397
|
+
single._workflowEnd === true &&
|
|
3398
|
+
single.output &&
|
|
3399
|
+
typeof single.output === "object" &&
|
|
3400
|
+
!Array.isArray(single.output)
|
|
3401
|
+
) {
|
|
3402
|
+
extracted = { ...single.output };
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
} else {
|
|
3406
|
+
const completedNodeIds = Array.from(childCtx?.nodeStatuses?.entries?.() || [])
|
|
3407
|
+
.filter(([, status]) => status === NodeStatus.COMPLETED)
|
|
3408
|
+
.map(([childNodeId]) => childNodeId);
|
|
3409
|
+
const lastCompletedNodeId = completedNodeIds[completedNodeIds.length - 1];
|
|
3410
|
+
if (lastCompletedNodeId && Object.prototype.hasOwnProperty.call(childOutputs, lastCompletedNodeId)) {
|
|
3411
|
+
const candidate = childOutputs[lastCompletedNodeId];
|
|
3412
|
+
if (
|
|
3413
|
+
candidate &&
|
|
3414
|
+
typeof candidate === "object" &&
|
|
3415
|
+
candidate._workflowEnd === true &&
|
|
3416
|
+
candidate.output &&
|
|
3417
|
+
typeof candidate.output === "object" &&
|
|
3418
|
+
!Array.isArray(candidate.output)
|
|
3419
|
+
) {
|
|
3420
|
+
extracted = { ...candidate.output };
|
|
3421
|
+
} else if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
3422
|
+
extracted = { ...candidate };
|
|
3423
|
+
} else {
|
|
3424
|
+
extracted = { result: candidate };
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
if (forwardFields.length > 0 && extracted && typeof extracted === "object") {
|
|
3430
|
+
return Object.fromEntries(
|
|
3431
|
+
forwardFields
|
|
3432
|
+
.filter((field) => Object.prototype.hasOwnProperty.call(extracted, field))
|
|
3433
|
+
.map((field) => [field, extracted[field]]),
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
return extracted;
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3439
|
+
if (mode === "dispatch") {
|
|
3440
|
+
ctx.log(node.id, `Dispatching inline workflow "${inlineWorkflowId}"`);
|
|
3441
|
+
let dispatched;
|
|
3442
|
+
try {
|
|
3443
|
+
dispatched = Promise.resolve(executeInline());
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
dispatched = Promise.reject(err);
|
|
3446
|
+
}
|
|
3447
|
+
dispatched
|
|
3448
|
+
.then((childCtx) => {
|
|
3449
|
+
const status = childCtx?.errors?.length ? "failed" : "completed";
|
|
3450
|
+
ctx.log(node.id, `Dispatched inline workflow "${inlineWorkflowId}" finished with status=${status}`);
|
|
3451
|
+
})
|
|
3452
|
+
.catch((err) => {
|
|
3453
|
+
ctx.log(node.id, `Dispatched inline workflow "${inlineWorkflowId}" failed: ${err.message}`, "error");
|
|
3454
|
+
});
|
|
3455
|
+
|
|
3456
|
+
const output = {
|
|
3457
|
+
success: true,
|
|
3458
|
+
dispatched: true,
|
|
3459
|
+
mode: "dispatch",
|
|
3460
|
+
workflowId: inlineWorkflowId,
|
|
3461
|
+
matchedPort: "default",
|
|
3462
|
+
port: "default",
|
|
3463
|
+
};
|
|
3464
|
+
if (outputVariable) {
|
|
3465
|
+
ctx.data[outputVariable] = output;
|
|
3466
|
+
}
|
|
3467
|
+
return output;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
ctx.log(node.id, `Executing inline workflow "${inlineWorkflowId}" (sync)`);
|
|
3471
|
+
const childCtx = await executeInline();
|
|
3472
|
+
const childErrors = Array.isArray(childCtx?.errors)
|
|
3473
|
+
? childCtx.errors.map((entry) => ({
|
|
3474
|
+
nodeId: entry?.nodeId || null,
|
|
3475
|
+
error: String(entry?.error || "unknown child workflow error"),
|
|
3476
|
+
}))
|
|
3477
|
+
: [];
|
|
3478
|
+
const status = childErrors.length > 0 ? "failed" : "completed";
|
|
3479
|
+
const extracted = extractChildOutputs(childCtx);
|
|
3480
|
+
const output = {
|
|
3481
|
+
success: status === "completed",
|
|
3482
|
+
dispatched: false,
|
|
3483
|
+
mode: "sync",
|
|
3484
|
+
workflowId: inlineWorkflowId,
|
|
3485
|
+
runId: childCtx?.id || null,
|
|
3486
|
+
status,
|
|
3487
|
+
errorCount: childErrors.length,
|
|
3488
|
+
errors: childErrors,
|
|
3489
|
+
matchedPort: status === "completed" ? "default" : "error",
|
|
3490
|
+
port: status === "completed" ? "default" : "error",
|
|
3491
|
+
childOutputs: extracted,
|
|
3492
|
+
...(extracted && typeof extracted === "object" ? extracted : {}),
|
|
3493
|
+
};
|
|
3494
|
+
|
|
3495
|
+
if (outputVariable) {
|
|
3496
|
+
ctx.data[outputVariable] = output;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
if (status === "failed" && failOnChildError) {
|
|
3500
|
+
const reason = childErrors[0]?.error || "inline workflow failed";
|
|
3501
|
+
const err = new Error(`action.inline_workflow: child inline workflow "${inlineWorkflowId}" failed: ${reason}`);
|
|
3502
|
+
err.childWorkflow = output;
|
|
3503
|
+
throw err;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
return output;
|
|
3507
|
+
},
|
|
3508
|
+
});
|
|
3509
|
+
|
|
3510
|
+
registerBuiltinNodeType("meeting.start", {
|
|
2755
3511
|
describe: () => "Create or reuse a meeting session for workflow-driven voice/video orchestration",
|
|
2756
3512
|
schema: {
|
|
2757
3513
|
type: "object",
|
|
@@ -2831,7 +3587,7 @@ registerNodeType("meeting.start", {
|
|
|
2831
3587
|
},
|
|
2832
3588
|
});
|
|
2833
3589
|
|
|
2834
|
-
|
|
3590
|
+
registerBuiltinNodeType("meeting.send", {
|
|
2835
3591
|
describe: () => "Send a meeting message through the meeting session dispatcher",
|
|
2836
3592
|
schema: {
|
|
2837
3593
|
type: "object",
|
|
@@ -2908,7 +3664,7 @@ registerNodeType("meeting.send", {
|
|
|
2908
3664
|
},
|
|
2909
3665
|
});
|
|
2910
3666
|
|
|
2911
|
-
|
|
3667
|
+
registerBuiltinNodeType("meeting.transcript", {
|
|
2912
3668
|
describe: () => "Fetch meeting transcript pages and optionally project as plain text",
|
|
2913
3669
|
schema: {
|
|
2914
3670
|
type: "object",
|
|
@@ -2979,7 +3735,7 @@ registerNodeType("meeting.transcript", {
|
|
|
2979
3735
|
},
|
|
2980
3736
|
});
|
|
2981
3737
|
|
|
2982
|
-
|
|
3738
|
+
registerBuiltinNodeType("meeting.vision", {
|
|
2983
3739
|
describe: () => "Analyze a meeting video frame and persist a vision summary",
|
|
2984
3740
|
schema: {
|
|
2985
3741
|
type: "object",
|
|
@@ -3076,7 +3832,7 @@ registerNodeType("meeting.vision", {
|
|
|
3076
3832
|
},
|
|
3077
3833
|
});
|
|
3078
3834
|
|
|
3079
|
-
|
|
3835
|
+
registerBuiltinNodeType("meeting.finalize", {
|
|
3080
3836
|
describe: () => "Finalize a meeting session with status and optional note",
|
|
3081
3837
|
schema: {
|
|
3082
3838
|
type: "object",
|
|
@@ -3128,7 +3884,7 @@ registerNodeType("meeting.finalize", {
|
|
|
3128
3884
|
},
|
|
3129
3885
|
});
|
|
3130
3886
|
|
|
3131
|
-
|
|
3887
|
+
registerBuiltinNodeType("action.create_task", {
|
|
3132
3888
|
describe: () => "Create a new task in the kanban board",
|
|
3133
3889
|
schema: {
|
|
3134
3890
|
type: "object",
|
|
@@ -3174,7 +3930,7 @@ registerNodeType("action.create_task", {
|
|
|
3174
3930
|
},
|
|
3175
3931
|
});
|
|
3176
3932
|
|
|
3177
|
-
|
|
3933
|
+
registerBuiltinNodeType("action.update_task_status", {
|
|
3178
3934
|
describe: () => "Update the status of an existing task",
|
|
3179
3935
|
schema: {
|
|
3180
3936
|
type: "object",
|
|
@@ -3323,7 +4079,7 @@ registerNodeType("action.update_task_status", {
|
|
|
3323
4079
|
},
|
|
3324
4080
|
});
|
|
3325
4081
|
|
|
3326
|
-
|
|
4082
|
+
registerBuiltinNodeType("action.git_operations", {
|
|
3327
4083
|
describe: () => "Perform git operations (commit, push, create branch, etc.)",
|
|
3328
4084
|
schema: {
|
|
3329
4085
|
type: "object",
|
|
@@ -3423,7 +4179,7 @@ registerNodeType("action.git_operations", {
|
|
|
3423
4179
|
},
|
|
3424
4180
|
});
|
|
3425
4181
|
|
|
3426
|
-
|
|
4182
|
+
registerBuiltinNodeType("action.create_pr", {
|
|
3427
4183
|
describe: () =>
|
|
3428
4184
|
"Create a pull request via GitHub CLI. Falls back to Bosun-managed handoff " +
|
|
3429
4185
|
"when gh is unavailable or the operation fails with failOnError=false.",
|
|
@@ -3445,6 +4201,26 @@ registerNodeType("action.create_pr", {
|
|
|
3445
4201
|
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
3446
4202
|
description: "Comma-separated or array of reviewer handles",
|
|
3447
4203
|
},
|
|
4204
|
+
enableAutoMerge: {
|
|
4205
|
+
type: "boolean",
|
|
4206
|
+
default: false,
|
|
4207
|
+
description: "Enable gh auto-merge immediately after PR creation/linking",
|
|
4208
|
+
},
|
|
4209
|
+
autoMerge: {
|
|
4210
|
+
type: "boolean",
|
|
4211
|
+
description: "Legacy alias for enableAutoMerge",
|
|
4212
|
+
},
|
|
4213
|
+
autoMergeMethod: {
|
|
4214
|
+
type: "string",
|
|
4215
|
+
enum: ["merge", "squash", "rebase"],
|
|
4216
|
+
default: "squash",
|
|
4217
|
+
description: "Merge method used with gh pr merge --auto",
|
|
4218
|
+
},
|
|
4219
|
+
mergeMethod: {
|
|
4220
|
+
type: "string",
|
|
4221
|
+
enum: ["merge", "squash", "rebase"],
|
|
4222
|
+
description: "Legacy alias for autoMergeMethod",
|
|
4223
|
+
},
|
|
3448
4224
|
cwd: { type: "string" },
|
|
3449
4225
|
failOnError: { type: "boolean", default: false, description: "If true, throw on gh failure instead of falling back" },
|
|
3450
4226
|
},
|
|
@@ -3460,6 +4236,16 @@ registerNodeType("action.create_pr", {
|
|
|
3460
4236
|
).trim();
|
|
3461
4237
|
const draft = node.config?.draft === true;
|
|
3462
4238
|
const failOnError = node.config?.failOnError === true;
|
|
4239
|
+
const enableAutoMerge = parseBooleanSetting(
|
|
4240
|
+
resolveWorkflowNodeValue(node.config?.enableAutoMerge ?? node.config?.autoMerge ?? false, ctx),
|
|
4241
|
+
false,
|
|
4242
|
+
);
|
|
4243
|
+
const autoMergeMethodRaw = String(
|
|
4244
|
+
ctx.resolve(node.config?.autoMergeMethod || node.config?.mergeMethod || "squash"),
|
|
4245
|
+
).trim().toLowerCase();
|
|
4246
|
+
const autoMergeMethod = ["merge", "squash", "rebase"].includes(autoMergeMethodRaw)
|
|
4247
|
+
? autoMergeMethodRaw
|
|
4248
|
+
: "squash";
|
|
3463
4249
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
3464
4250
|
|
|
3465
4251
|
// Normalize labels/reviewers to arrays
|
|
@@ -3502,6 +4288,46 @@ registerNodeType("action.create_pr", {
|
|
|
3502
4288
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3503
4289
|
};
|
|
3504
4290
|
|
|
4291
|
+
const maybeEnableAutoMerge = (prNumber) => {
|
|
4292
|
+
if (!enableAutoMerge) {
|
|
4293
|
+
return { enabled: false, attempted: false, success: false };
|
|
4294
|
+
}
|
|
4295
|
+
if (draft) {
|
|
4296
|
+
return { enabled: true, attempted: false, success: false, reason: "draft_pr", method: autoMergeMethod };
|
|
4297
|
+
}
|
|
4298
|
+
const parsedPrNumber = Number.parseInt(String(prNumber || ""), 10);
|
|
4299
|
+
if (!Number.isFinite(parsedPrNumber) || parsedPrNumber <= 0) {
|
|
4300
|
+
return { enabled: true, attempted: false, success: false, reason: "missing_pr_number", method: autoMergeMethod };
|
|
4301
|
+
}
|
|
4302
|
+
if (shouldBypassGhPrCreationForTests()) {
|
|
4303
|
+
return { enabled: true, attempted: false, success: false, reason: "test_runtime_skip", method: autoMergeMethod };
|
|
4304
|
+
}
|
|
4305
|
+
try {
|
|
4306
|
+
const mergeArgs = ["pr", "merge", String(parsedPrNumber), "--auto", `--${autoMergeMethod}`];
|
|
4307
|
+
if (repoSlug) mergeArgs.push("--repo", repoSlug);
|
|
4308
|
+
execFileSync("gh", mergeArgs, execOptions);
|
|
4309
|
+
ctx.log(node.id, `Auto-merge requested for PR #${parsedPrNumber} (${autoMergeMethod})`);
|
|
4310
|
+
return {
|
|
4311
|
+
enabled: true,
|
|
4312
|
+
attempted: true,
|
|
4313
|
+
success: true,
|
|
4314
|
+
method: autoMergeMethod,
|
|
4315
|
+
prNumber: parsedPrNumber,
|
|
4316
|
+
};
|
|
4317
|
+
} catch (err) {
|
|
4318
|
+
const error = err?.stderr?.toString?.()?.trim() || err?.message || String(err);
|
|
4319
|
+
ctx.log(node.id, `Auto-merge request failed for PR #${parsedPrNumber}: ${error}`);
|
|
4320
|
+
return {
|
|
4321
|
+
enabled: true,
|
|
4322
|
+
attempted: true,
|
|
4323
|
+
success: false,
|
|
4324
|
+
method: autoMergeMethod,
|
|
4325
|
+
prNumber: parsedPrNumber,
|
|
4326
|
+
error,
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
|
|
3505
4331
|
/** Re-resolve token after invalidating the current one (401 retry). */
|
|
3506
4332
|
const retryWithFallbackToken = async () => {
|
|
3507
4333
|
if (!resolvedTokenType) return false;
|
|
@@ -3557,6 +4383,7 @@ registerNodeType("action.create_pr", {
|
|
|
3557
4383
|
} catch {
|
|
3558
4384
|
}
|
|
3559
4385
|
}
|
|
4386
|
+
const autoMergeState = maybeEnableAutoMerge(prNumber);
|
|
3560
4387
|
return {
|
|
3561
4388
|
success: true,
|
|
3562
4389
|
existing: true,
|
|
@@ -3570,6 +4397,7 @@ registerNodeType("action.create_pr", {
|
|
|
3570
4397
|
labels,
|
|
3571
4398
|
reviewers,
|
|
3572
4399
|
output: String(existing?.url || `existing-pr-${prNumber}`),
|
|
4400
|
+
autoMerge: autoMergeState,
|
|
3573
4401
|
};
|
|
3574
4402
|
} catch {
|
|
3575
4403
|
return null;
|
|
@@ -3610,6 +4438,13 @@ registerNodeType("action.create_pr", {
|
|
|
3610
4438
|
cwd,
|
|
3611
4439
|
repoSlug: repoSlug || null,
|
|
3612
4440
|
ghError: "skipped_in_test_runtime",
|
|
4441
|
+
autoMerge: {
|
|
4442
|
+
enabled: enableAutoMerge,
|
|
4443
|
+
attempted: false,
|
|
4444
|
+
success: false,
|
|
4445
|
+
reason: "test_runtime_skip",
|
|
4446
|
+
method: autoMergeMethod,
|
|
4447
|
+
},
|
|
3613
4448
|
};
|
|
3614
4449
|
}
|
|
3615
4450
|
|
|
@@ -3620,6 +4455,7 @@ registerNodeType("action.create_pr", {
|
|
|
3620
4455
|
const urlMatch = trimmed.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
|
|
3621
4456
|
const prNumber = urlMatch ? parseInt(urlMatch[1], 10) : null;
|
|
3622
4457
|
const prUrl = urlMatch ? urlMatch[0] : trimmed;
|
|
4458
|
+
const autoMergeState = maybeEnableAutoMerge(prNumber);
|
|
3623
4459
|
ctx.log(node.id, `PR created: ${prUrl}`);
|
|
3624
4460
|
return {
|
|
3625
4461
|
success: true,
|
|
@@ -3633,6 +4469,7 @@ registerNodeType("action.create_pr", {
|
|
|
3633
4469
|
labels,
|
|
3634
4470
|
reviewers,
|
|
3635
4471
|
output: trimmed,
|
|
4472
|
+
autoMerge: autoMergeState,
|
|
3636
4473
|
};
|
|
3637
4474
|
} catch (err) {
|
|
3638
4475
|
const errorMsg = err?.stderr?.toString?.()?.trim() || err?.message || String(err);
|
|
@@ -3649,6 +4486,7 @@ registerNodeType("action.create_pr", {
|
|
|
3649
4486
|
const urlMatch = trimmed.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
|
|
3650
4487
|
const prNumber = urlMatch ? parseInt(urlMatch[1], 10) : null;
|
|
3651
4488
|
const prUrl = urlMatch ? urlMatch[0] : trimmed;
|
|
4489
|
+
const autoMergeState = maybeEnableAutoMerge(prNumber);
|
|
3652
4490
|
ctx.log(node.id, `PR created (after auth retry): ${prUrl}`);
|
|
3653
4491
|
return {
|
|
3654
4492
|
success: true,
|
|
@@ -3662,6 +4500,7 @@ registerNodeType("action.create_pr", {
|
|
|
3662
4500
|
labels,
|
|
3663
4501
|
reviewers,
|
|
3664
4502
|
output: trimmed,
|
|
4503
|
+
autoMerge: autoMergeState,
|
|
3665
4504
|
};
|
|
3666
4505
|
} catch (retryErr) {
|
|
3667
4506
|
const retryMsg = retryErr?.stderr?.toString?.()?.trim() || retryErr?.message || String(retryErr);
|
|
@@ -3701,12 +4540,19 @@ registerNodeType("action.create_pr", {
|
|
|
3701
4540
|
reviewers,
|
|
3702
4541
|
cwd,
|
|
3703
4542
|
ghError: errorMsg,
|
|
4543
|
+
autoMerge: {
|
|
4544
|
+
enabled: enableAutoMerge,
|
|
4545
|
+
attempted: false,
|
|
4546
|
+
success: false,
|
|
4547
|
+
reason: "pr_creation_failed",
|
|
4548
|
+
method: autoMergeMethod,
|
|
4549
|
+
},
|
|
3704
4550
|
};
|
|
3705
4551
|
}
|
|
3706
4552
|
},
|
|
3707
4553
|
});
|
|
3708
4554
|
|
|
3709
|
-
|
|
4555
|
+
registerBuiltinNodeType("action.write_file", {
|
|
3710
4556
|
describe: () => "Write content to a file in the workspace",
|
|
3711
4557
|
schema: {
|
|
3712
4558
|
type: "object",
|
|
@@ -3735,7 +4581,7 @@ registerNodeType("action.write_file", {
|
|
|
3735
4581
|
},
|
|
3736
4582
|
});
|
|
3737
4583
|
|
|
3738
|
-
|
|
4584
|
+
registerBuiltinNodeType("action.read_file", {
|
|
3739
4585
|
describe: () => "Read content from a file",
|
|
3740
4586
|
schema: {
|
|
3741
4587
|
type: "object",
|
|
@@ -3754,7 +4600,7 @@ registerNodeType("action.read_file", {
|
|
|
3754
4600
|
},
|
|
3755
4601
|
});
|
|
3756
4602
|
|
|
3757
|
-
|
|
4603
|
+
registerBuiltinNodeType("action.set_variable", {
|
|
3758
4604
|
describe: () => "Set a variable in the workflow context for downstream nodes",
|
|
3759
4605
|
schema: {
|
|
3760
4606
|
type: "object",
|
|
@@ -3784,7 +4630,7 @@ registerNodeType("action.set_variable", {
|
|
|
3784
4630
|
},
|
|
3785
4631
|
});
|
|
3786
4632
|
|
|
3787
|
-
|
|
4633
|
+
registerBuiltinNodeType("action.delay", {
|
|
3788
4634
|
describe: () => "Wait for a specified duration before continuing (supports ms, seconds, minutes, hours)",
|
|
3789
4635
|
schema: {
|
|
3790
4636
|
type: "object",
|
|
@@ -3837,7 +4683,7 @@ registerNodeType("action.delay", {
|
|
|
3837
4683
|
// VALIDATION — Verification gates
|
|
3838
4684
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3839
4685
|
|
|
3840
|
-
|
|
4686
|
+
registerBuiltinNodeType("validation.screenshot", {
|
|
3841
4687
|
describe: () => "Take a screenshot for visual verification and store in evidence",
|
|
3842
4688
|
schema: {
|
|
3843
4689
|
type: "object",
|
|
@@ -3951,7 +4797,7 @@ registerNodeType("validation.screenshot", {
|
|
|
3951
4797
|
},
|
|
3952
4798
|
});
|
|
3953
4799
|
|
|
3954
|
-
|
|
4800
|
+
registerBuiltinNodeType("validation.model_review", {
|
|
3955
4801
|
describe: () => "Send evidence (screenshots, code, logs) to a non-agent model for independent verification",
|
|
3956
4802
|
schema: {
|
|
3957
4803
|
type: "object",
|
|
@@ -4069,7 +4915,7 @@ Respond with exactly one of:
|
|
|
4069
4915
|
},
|
|
4070
4916
|
});
|
|
4071
4917
|
|
|
4072
|
-
|
|
4918
|
+
registerBuiltinNodeType("validation.tests", {
|
|
4073
4919
|
describe: () => "Run test suite and verify results",
|
|
4074
4920
|
schema: {
|
|
4075
4921
|
type: "object",
|
|
@@ -4098,7 +4944,7 @@ registerNodeType("validation.tests", {
|
|
|
4098
4944
|
},
|
|
4099
4945
|
});
|
|
4100
4946
|
|
|
4101
|
-
|
|
4947
|
+
registerBuiltinNodeType("validation.build", {
|
|
4102
4948
|
describe: () => "Run build and verify it succeeds with 0 errors",
|
|
4103
4949
|
schema: {
|
|
4104
4950
|
type: "object",
|
|
@@ -4132,7 +4978,7 @@ registerNodeType("validation.build", {
|
|
|
4132
4978
|
},
|
|
4133
4979
|
});
|
|
4134
4980
|
|
|
4135
|
-
|
|
4981
|
+
registerBuiltinNodeType("validation.lint", {
|
|
4136
4982
|
describe: () => "Run linter and verify results",
|
|
4137
4983
|
schema: {
|
|
4138
4984
|
type: "object",
|
|
@@ -4161,7 +5007,7 @@ registerNodeType("validation.lint", {
|
|
|
4161
5007
|
// TRANSFORM — Data manipulation
|
|
4162
5008
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4163
5009
|
|
|
4164
|
-
|
|
5010
|
+
registerBuiltinNodeType("transform.json_parse", {
|
|
4165
5011
|
describe: () => "Parse JSON from a previous node's output",
|
|
4166
5012
|
schema: {
|
|
4167
5013
|
type: "object",
|
|
@@ -4183,7 +5029,7 @@ registerNodeType("transform.json_parse", {
|
|
|
4183
5029
|
},
|
|
4184
5030
|
});
|
|
4185
5031
|
|
|
4186
|
-
|
|
5032
|
+
registerBuiltinNodeType("transform.template", {
|
|
4187
5033
|
describe: () => "Render a text template with context variables",
|
|
4188
5034
|
schema: {
|
|
4189
5035
|
type: "object",
|
|
@@ -4198,7 +5044,7 @@ registerNodeType("transform.template", {
|
|
|
4198
5044
|
},
|
|
4199
5045
|
});
|
|
4200
5046
|
|
|
4201
|
-
|
|
5047
|
+
registerBuiltinNodeType("transform.aggregate", {
|
|
4202
5048
|
describe: () => "Aggregate outputs from multiple nodes into a single object",
|
|
4203
5049
|
schema: {
|
|
4204
5050
|
type: "object",
|
|
@@ -4216,7 +5062,7 @@ registerNodeType("transform.aggregate", {
|
|
|
4216
5062
|
},
|
|
4217
5063
|
});
|
|
4218
5064
|
|
|
4219
|
-
|
|
5065
|
+
registerBuiltinNodeType("transform.llm_parse", {
|
|
4220
5066
|
describe: () =>
|
|
4221
5067
|
"Parse unstructured LLM output into structured fields using regex patterns " +
|
|
4222
5068
|
"or keyword extraction. Essential for routing decisions based on LLM verdicts " +
|
|
@@ -4327,7 +5173,7 @@ registerNodeType("transform.llm_parse", {
|
|
|
4327
5173
|
// NOTIFY — Notifications
|
|
4328
5174
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4329
5175
|
|
|
4330
|
-
|
|
5176
|
+
registerBuiltinNodeType("notify.log", {
|
|
4331
5177
|
describe: () => "Log a message (to console and workflow run log)",
|
|
4332
5178
|
schema: {
|
|
4333
5179
|
type: "object",
|
|
@@ -4346,7 +5192,7 @@ registerNodeType("notify.log", {
|
|
|
4346
5192
|
},
|
|
4347
5193
|
});
|
|
4348
5194
|
|
|
4349
|
-
|
|
5195
|
+
registerBuiltinNodeType("notify.telegram", {
|
|
4350
5196
|
describe: () => "Send a message to Telegram chat",
|
|
4351
5197
|
schema: {
|
|
4352
5198
|
type: "object",
|
|
@@ -4374,42 +5220,154 @@ registerNodeType("notify.telegram", {
|
|
|
4374
5220
|
);
|
|
4375
5221
|
return { sent: true, message };
|
|
4376
5222
|
}
|
|
4377
|
-
ctx.log(node.id, "Telegram service not available", "warn");
|
|
4378
|
-
return { sent: false, reason: "no_telegram" };
|
|
4379
|
-
},
|
|
4380
|
-
});
|
|
5223
|
+
ctx.log(node.id, "Telegram service not available", "warn");
|
|
5224
|
+
return { sent: false, reason: "no_telegram" };
|
|
5225
|
+
},
|
|
5226
|
+
});
|
|
5227
|
+
|
|
5228
|
+
registerBuiltinNodeType("notify.webhook_out", {
|
|
5229
|
+
describe: () => "Send an HTTP webhook notification",
|
|
5230
|
+
schema: {
|
|
5231
|
+
type: "object",
|
|
5232
|
+
properties: {
|
|
5233
|
+
url: { type: "string", description: "Webhook URL" },
|
|
5234
|
+
method: { type: "string", default: "POST" },
|
|
5235
|
+
body: { type: "object", description: "Request body (supports {{variables}} in string values)" },
|
|
5236
|
+
headers: { type: "object" },
|
|
5237
|
+
},
|
|
5238
|
+
required: ["url"],
|
|
5239
|
+
},
|
|
5240
|
+
async execute(node, ctx) {
|
|
5241
|
+
const url = ctx.resolve(node.config?.url || "");
|
|
5242
|
+
const method = node.config?.method || "POST";
|
|
5243
|
+
const body = node.config?.body ? JSON.stringify(node.config.body) : undefined;
|
|
5244
|
+
|
|
5245
|
+
ctx.log(node.id, `Webhook ${method} to ${url}`);
|
|
5246
|
+
try {
|
|
5247
|
+
const resp = await fetch(url, {
|
|
5248
|
+
method,
|
|
5249
|
+
headers: {
|
|
5250
|
+
"Content-Type": "application/json",
|
|
5251
|
+
...node.config?.headers,
|
|
5252
|
+
},
|
|
5253
|
+
body,
|
|
5254
|
+
});
|
|
5255
|
+
return { success: resp.ok, status: resp.status };
|
|
5256
|
+
} catch (err) {
|
|
5257
|
+
return { success: false, error: err.message };
|
|
5258
|
+
}
|
|
5259
|
+
},
|
|
5260
|
+
});
|
|
5261
|
+
|
|
5262
|
+
registerNodeType("action.emit_event", {
|
|
5263
|
+
describe: () =>
|
|
5264
|
+
"Emit an internal workflow event and optionally dispatch matching trigger.event workflows",
|
|
5265
|
+
schema: {
|
|
5266
|
+
type: "object",
|
|
5267
|
+
properties: {
|
|
5268
|
+
eventType: { type: "string", description: "Event type to emit (for example session-stuck)" },
|
|
5269
|
+
payload: {
|
|
5270
|
+
type: "object",
|
|
5271
|
+
description: "Event payload object forwarded to matching workflows",
|
|
5272
|
+
additionalProperties: true,
|
|
5273
|
+
},
|
|
5274
|
+
dispatch: {
|
|
5275
|
+
type: "boolean",
|
|
5276
|
+
default: true,
|
|
5277
|
+
description: "When true, evaluate and execute matching event-trigger workflows",
|
|
5278
|
+
},
|
|
5279
|
+
includeCurrentWorkflow: {
|
|
5280
|
+
type: "boolean",
|
|
5281
|
+
default: false,
|
|
5282
|
+
description: "Allow dispatching the currently running workflow if it matches",
|
|
5283
|
+
},
|
|
5284
|
+
outputVariable: {
|
|
5285
|
+
type: "string",
|
|
5286
|
+
description: "Optional context key where event output will be stored",
|
|
5287
|
+
},
|
|
5288
|
+
},
|
|
5289
|
+
required: ["eventType"],
|
|
5290
|
+
},
|
|
5291
|
+
async execute(node, ctx, engine) {
|
|
5292
|
+
const eventType = String(ctx.resolve(node.config?.eventType || "") || "").trim();
|
|
5293
|
+
if (!eventType) throw new Error("action.emit_event: 'eventType' is required");
|
|
5294
|
+
|
|
5295
|
+
const payload = resolveWorkflowNodeValue(node.config?.payload ?? {}, ctx);
|
|
5296
|
+
const shouldDispatch = parseBooleanSetting(
|
|
5297
|
+
resolveWorkflowNodeValue(node.config?.dispatch ?? true, ctx),
|
|
5298
|
+
true,
|
|
5299
|
+
);
|
|
5300
|
+
const includeCurrentWorkflow = parseBooleanSetting(
|
|
5301
|
+
resolveWorkflowNodeValue(node.config?.includeCurrentWorkflow ?? false, ctx),
|
|
5302
|
+
false,
|
|
5303
|
+
);
|
|
5304
|
+
const currentWorkflowId = String(ctx.data?._workflowId || "").trim();
|
|
5305
|
+
|
|
5306
|
+
const output = {
|
|
5307
|
+
success: true,
|
|
5308
|
+
eventType,
|
|
5309
|
+
payload,
|
|
5310
|
+
dispatched: false,
|
|
5311
|
+
dispatchCount: 0,
|
|
5312
|
+
matched: [],
|
|
5313
|
+
runs: [],
|
|
5314
|
+
};
|
|
5315
|
+
|
|
5316
|
+
if (shouldDispatch && engine?.evaluateTriggers && engine?.execute) {
|
|
5317
|
+
const matched = await engine.evaluateTriggers(eventType, payload || {});
|
|
5318
|
+
output.matched = matched;
|
|
5319
|
+
for (const trigger of matched) {
|
|
5320
|
+
const workflowId = String(trigger?.workflowId || "").trim();
|
|
5321
|
+
if (!workflowId) continue;
|
|
5322
|
+
if (!includeCurrentWorkflow && currentWorkflowId && workflowId === currentWorkflowId) {
|
|
5323
|
+
continue;
|
|
5324
|
+
}
|
|
5325
|
+
try {
|
|
5326
|
+
const childCtx = await engine.execute(
|
|
5327
|
+
workflowId,
|
|
5328
|
+
{
|
|
5329
|
+
...(payload && typeof payload === "object" ? payload : {}),
|
|
5330
|
+
eventType,
|
|
5331
|
+
_triggerSource: "workflow.emit_event",
|
|
5332
|
+
_triggeredByWorkflowId: currentWorkflowId || null,
|
|
5333
|
+
_triggeredByRunId: ctx.id,
|
|
5334
|
+
},
|
|
5335
|
+
{ force: true },
|
|
5336
|
+
);
|
|
5337
|
+
const childErrors = Array.isArray(childCtx?.errors) ? childCtx.errors : [];
|
|
5338
|
+
output.runs.push({
|
|
5339
|
+
workflowId,
|
|
5340
|
+
runId: childCtx?.id || null,
|
|
5341
|
+
status: childErrors.length > 0 ? "failed" : "completed",
|
|
5342
|
+
});
|
|
5343
|
+
} catch (err) {
|
|
5344
|
+
output.runs.push({
|
|
5345
|
+
workflowId,
|
|
5346
|
+
runId: null,
|
|
5347
|
+
status: "failed",
|
|
5348
|
+
error: err?.message || String(err),
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
output.dispatchCount = output.runs.length;
|
|
5353
|
+
output.dispatched = output.dispatchCount > 0;
|
|
5354
|
+
}
|
|
4381
5355
|
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
properties: {
|
|
4387
|
-
url: { type: "string", description: "Webhook URL" },
|
|
4388
|
-
method: { type: "string", default: "POST" },
|
|
4389
|
-
body: { type: "object", description: "Request body (supports {{variables}} in string values)" },
|
|
4390
|
-
headers: { type: "object" },
|
|
4391
|
-
},
|
|
4392
|
-
required: ["url"],
|
|
4393
|
-
},
|
|
4394
|
-
async execute(node, ctx) {
|
|
4395
|
-
const url = ctx.resolve(node.config?.url || "");
|
|
4396
|
-
const method = node.config?.method || "POST";
|
|
4397
|
-
const body = node.config?.body ? JSON.stringify(node.config.body) : undefined;
|
|
5356
|
+
if (ctx?.data && typeof ctx.data === "object") {
|
|
5357
|
+
ctx.data.eventType = eventType;
|
|
5358
|
+
ctx.data.eventPayload = payload;
|
|
5359
|
+
}
|
|
4398
5360
|
|
|
4399
|
-
ctx.
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
method,
|
|
4403
|
-
headers: {
|
|
4404
|
-
"Content-Type": "application/json",
|
|
4405
|
-
...node.config?.headers,
|
|
4406
|
-
},
|
|
4407
|
-
body,
|
|
4408
|
-
});
|
|
4409
|
-
return { success: resp.ok, status: resp.status };
|
|
4410
|
-
} catch (err) {
|
|
4411
|
-
return { success: false, error: err.message };
|
|
5361
|
+
const outputVariable = String(ctx.resolve(node.config?.outputVariable || "") || "").trim();
|
|
5362
|
+
if (outputVariable) {
|
|
5363
|
+
ctx.data[outputVariable] = output;
|
|
4412
5364
|
}
|
|
5365
|
+
|
|
5366
|
+
ctx.log(
|
|
5367
|
+
node.id,
|
|
5368
|
+
`Emitted event ${eventType} (dispatch=${output.dispatched}, runs=${output.dispatchCount})`,
|
|
5369
|
+
);
|
|
5370
|
+
return output;
|
|
4413
5371
|
},
|
|
4414
5372
|
});
|
|
4415
5373
|
|
|
@@ -4417,7 +5375,7 @@ registerNodeType("notify.webhook_out", {
|
|
|
4417
5375
|
// AGENT-SPECIFIC — Specialized agent operations
|
|
4418
5376
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4419
5377
|
|
|
4420
|
-
|
|
5378
|
+
registerBuiltinNodeType("agent.select_profile", {
|
|
4421
5379
|
describe: () => "Select an agent profile based on task characteristics",
|
|
4422
5380
|
schema: {
|
|
4423
5381
|
type: "object",
|
|
@@ -4659,8 +5617,50 @@ function normalizePlannerTaskForCreation(task, index) {
|
|
|
4659
5617
|
}
|
|
4660
5618
|
return normalized;
|
|
4661
5619
|
};
|
|
5620
|
+
const normalizeScore = (value) => {
|
|
5621
|
+
const numeric = Number(value);
|
|
5622
|
+
if (!Number.isFinite(numeric)) return null;
|
|
5623
|
+
return Math.max(0, Math.min(10, Math.round(numeric)));
|
|
5624
|
+
};
|
|
5625
|
+
const normalizeRiskLevel = (value) => {
|
|
5626
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
5627
|
+
if (["low", "medium", "high", "critical"].includes(raw)) return raw;
|
|
5628
|
+
const numeric = Number(value);
|
|
5629
|
+
if (!Number.isFinite(numeric)) return null;
|
|
5630
|
+
if (numeric >= 9) return "critical";
|
|
5631
|
+
if (numeric >= 7) return "high";
|
|
5632
|
+
if (numeric >= 4) return "medium";
|
|
5633
|
+
return "low";
|
|
5634
|
+
};
|
|
5635
|
+
const normalizeArchetype = (value) => {
|
|
5636
|
+
const normalized = String(value || "")
|
|
5637
|
+
.trim()
|
|
5638
|
+
.toLowerCase()
|
|
5639
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
5640
|
+
.replace(/^_+|_+$/g, "");
|
|
5641
|
+
return normalized || "";
|
|
5642
|
+
};
|
|
5643
|
+
const inferArchetype = () => {
|
|
5644
|
+
const explicit =
|
|
5645
|
+
task.archetype ||
|
|
5646
|
+
task.task_archetype ||
|
|
5647
|
+
task.taskArchetype ||
|
|
5648
|
+
task.pattern ||
|
|
5649
|
+
"";
|
|
5650
|
+
const normalizedExplicit = normalizeArchetype(explicit);
|
|
5651
|
+
if (normalizedExplicit) return normalizedExplicit;
|
|
5652
|
+
const conventional = title
|
|
5653
|
+
.toLowerCase()
|
|
5654
|
+
.match(/^(?:\[[^\]]+\]\s*)?([a-z][a-z0-9_-]*)(?:\([^)]*\))?:/);
|
|
5655
|
+
if (conventional?.[1]) return normalizeArchetype(conventional[1]);
|
|
5656
|
+
if (title.toLowerCase().includes("test")) return "test";
|
|
5657
|
+
if (title.toLowerCase().includes("doc")) return "docs";
|
|
5658
|
+
if (title.toLowerCase().includes("refactor")) return "refactor";
|
|
5659
|
+
return "general";
|
|
5660
|
+
};
|
|
4662
5661
|
const scoreMode = inferPlannerTaskScoreMode(task);
|
|
4663
5662
|
const preferTenScaleIntegers = scoreMode === PLANNER_SCORE_MODE_TEN;
|
|
5663
|
+
|
|
4664
5664
|
const lines = [];
|
|
4665
5665
|
const description = String(task.description || "").trim();
|
|
4666
5666
|
if (description) lines.push(description);
|
|
@@ -4676,6 +5676,7 @@ function normalizePlannerTaskForCreation(task, index) {
|
|
|
4676
5676
|
const estimatedEffort = String(task.estimated_effort || task.estimatedEffort || "").trim().toLowerCase();
|
|
4677
5677
|
const whyNow = String(task.why_now || task.whyNow || "").trim();
|
|
4678
5678
|
const killCriteria = normalizeStringList(task.kill_criteria || task.killCriteria);
|
|
5679
|
+
const archetype = inferArchetype();
|
|
4679
5680
|
|
|
4680
5681
|
const appendList = (heading, values) => {
|
|
4681
5682
|
if (!Array.isArray(values) || values.length === 0) return;
|
|
@@ -4727,6 +5728,7 @@ function normalizePlannerTaskForCreation(task, index) {
|
|
|
4727
5728
|
impact,
|
|
4728
5729
|
confidence,
|
|
4729
5730
|
risk,
|
|
5731
|
+
archetype,
|
|
4730
5732
|
estimatedEffort: estimatedEffort || null,
|
|
4731
5733
|
whyNow: whyNow || null,
|
|
4732
5734
|
killCriteria: killCriteria.length > 0 ? killCriteria : null,
|
|
@@ -4830,6 +5832,401 @@ function resolvePlannerFeedbackContext(value) {
|
|
|
4830
5832
|
return String(value).trim();
|
|
4831
5833
|
}
|
|
4832
5834
|
|
|
5835
|
+
function resolvePlannerFeedbackObject(value) {
|
|
5836
|
+
if (!value) return null;
|
|
5837
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
5838
|
+
if (typeof value !== "string") return null;
|
|
5839
|
+
const trimmed = value.trim();
|
|
5840
|
+
if (!trimmed) return null;
|
|
5841
|
+
try {
|
|
5842
|
+
const parsed = JSON.parse(trimmed);
|
|
5843
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
5844
|
+
} catch {
|
|
5845
|
+
return null;
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
|
|
5849
|
+
function normalizePlannerTaskArchetype(task) {
|
|
5850
|
+
const explicitArchetype = String(
|
|
5851
|
+
task?.archetype || task?.taskArchetype || task?.task_archetype || "",
|
|
5852
|
+
)
|
|
5853
|
+
.trim()
|
|
5854
|
+
.toLowerCase()
|
|
5855
|
+
.replace(/[^a-z0-9()_-]+/g, "_")
|
|
5856
|
+
.replace(/^_+|_+$/g, "");
|
|
5857
|
+
if (explicitArchetype) return explicitArchetype;
|
|
5858
|
+
const title = String(task?.title || "").trim().toLowerCase();
|
|
5859
|
+
if (!title) return "general";
|
|
5860
|
+
const withoutPrefix = title.replace(/^\[[^\]]+\]\s*/, "").trim();
|
|
5861
|
+
const scoped = withoutPrefix.match(/^([a-z][a-z0-9_-]*)\(([^)]+)\)\s*:/);
|
|
5862
|
+
if (scoped) return scoped[1];
|
|
5863
|
+
const typed = withoutPrefix.match(/^([a-z][a-z0-9_-]*)\s*:/);
|
|
5864
|
+
if (typed) return typed[1];
|
|
5865
|
+
const fallback = withoutPrefix
|
|
5866
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
5867
|
+
.trim()
|
|
5868
|
+
.split(/\s+/)
|
|
5869
|
+
.slice(0, 2)
|
|
5870
|
+
.join("_");
|
|
5871
|
+
return fallback || "general";
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
function resolvePlannerPatternKeys(task) {
|
|
5875
|
+
const archetype = normalizePlannerTaskArchetype(task);
|
|
5876
|
+
const areas = resolveTaskRepoAreas(task);
|
|
5877
|
+
const normalizedAreas = areas.length > 0
|
|
5878
|
+
? areas.map((area) => normalizePlannerAreaKey(area)).filter(Boolean)
|
|
5879
|
+
: ["global"];
|
|
5880
|
+
return normalizedAreas.map((area) => `${area}::${archetype}`);
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
function resolvePlannerDebtTrendSignal(task) {
|
|
5884
|
+
const numericCandidates = [
|
|
5885
|
+
task?.debt_trend,
|
|
5886
|
+
task?.debtTrend,
|
|
5887
|
+
task?.meta?.debt_trend,
|
|
5888
|
+
task?.meta?.debtTrend,
|
|
5889
|
+
task?.meta?.planner?.debt_trend,
|
|
5890
|
+
task?.meta?.planner?.debtTrend,
|
|
5891
|
+
task?.meta?.planner?.debt_growth,
|
|
5892
|
+
task?.meta?.planner?.debtGrowth,
|
|
5893
|
+
];
|
|
5894
|
+
for (const candidate of numericCandidates) {
|
|
5895
|
+
const numeric = Number(candidate);
|
|
5896
|
+
if (Number.isFinite(numeric)) {
|
|
5897
|
+
return Math.max(0, Math.min(5, Math.abs(numeric)));
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5901
|
+
const textCandidates = [
|
|
5902
|
+
task?.debt_trend,
|
|
5903
|
+
task?.debtTrend,
|
|
5904
|
+
task?.meta?.debt_trend,
|
|
5905
|
+
task?.meta?.debtTrend,
|
|
5906
|
+
task?.meta?.planner?.debt_trend,
|
|
5907
|
+
task?.meta?.planner?.debtTrend,
|
|
5908
|
+
task?.meta?.planner?.why_now,
|
|
5909
|
+
task?.meta?.planner?.whyNow,
|
|
5910
|
+
task?.description,
|
|
5911
|
+
]
|
|
5912
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
5913
|
+
.filter(Boolean);
|
|
5914
|
+
for (const text of textCandidates) {
|
|
5915
|
+
if (/(worsen|worsening|increase|increasing|growth|growing|upward|regress)/.test(text)) {
|
|
5916
|
+
return 2;
|
|
5917
|
+
}
|
|
5918
|
+
if (/(stable|flat|neutral|steady)/.test(text)) {
|
|
5919
|
+
return 1;
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
return 0;
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
function hasTaskCommitEvidence(task) {
|
|
5926
|
+
const commitCandidates = [
|
|
5927
|
+
task?.hasCommits,
|
|
5928
|
+
task?.meta?.hasCommits,
|
|
5929
|
+
task?.meta?.execution?.hasCommits,
|
|
5930
|
+
task?.meta?.execution?.commitCount,
|
|
5931
|
+
task?.meta?.execution?.commits,
|
|
5932
|
+
task?.commitCount,
|
|
5933
|
+
task?.commits,
|
|
5934
|
+
task?.meta?.commits,
|
|
5935
|
+
];
|
|
5936
|
+
for (const candidate of commitCandidates) {
|
|
5937
|
+
if (typeof candidate === "boolean") return candidate;
|
|
5938
|
+
const numeric = Number(candidate);
|
|
5939
|
+
if (Number.isFinite(numeric) && numeric > 0) return true;
|
|
5940
|
+
if (Array.isArray(candidate) && candidate.length > 0) return true;
|
|
5941
|
+
}
|
|
5942
|
+
return false;
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
function createEmptyPlannerPatternPrior() {
|
|
5946
|
+
return {
|
|
5947
|
+
failureCount: 0,
|
|
5948
|
+
successCount: 0,
|
|
5949
|
+
failureWeight: 0,
|
|
5950
|
+
successWeight: 0,
|
|
5951
|
+
failureCounter: 0,
|
|
5952
|
+
commitlessFailureCount: 0,
|
|
5953
|
+
commitlessSuccessCount: 0,
|
|
5954
|
+
commitlessFailureCounter: 0,
|
|
5955
|
+
signalTotals: {
|
|
5956
|
+
agentAttempts: 0,
|
|
5957
|
+
consecutiveNoCommits: 0,
|
|
5958
|
+
blockedReason: 0,
|
|
5959
|
+
debtTrend: 0,
|
|
5960
|
+
},
|
|
5961
|
+
lastUpdatedAt: null,
|
|
5962
|
+
};
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5965
|
+
function normalizePlannerPatternPrior(entry) {
|
|
5966
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
5967
|
+
return createEmptyPlannerPatternPrior();
|
|
5968
|
+
}
|
|
5969
|
+
const base = createEmptyPlannerPatternPrior();
|
|
5970
|
+
const signalTotals = entry.signalTotals && typeof entry.signalTotals === "object"
|
|
5971
|
+
? entry.signalTotals
|
|
5972
|
+
: {};
|
|
5973
|
+
return {
|
|
5974
|
+
...base,
|
|
5975
|
+
...entry,
|
|
5976
|
+
signalTotals: {
|
|
5977
|
+
agentAttempts: Number(signalTotals.agentAttempts || 0),
|
|
5978
|
+
consecutiveNoCommits: Number(signalTotals.consecutiveNoCommits || 0),
|
|
5979
|
+
blockedReason: Number(signalTotals.blockedReason || 0),
|
|
5980
|
+
debtTrend: Number(signalTotals.debtTrend || 0),
|
|
5981
|
+
},
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function resolvePlannerOutcomeSignals(task, weights) {
|
|
5986
|
+
const attempts = Math.max(0, Number(task?.agentAttempts || task?.meta?.agentAttempts || 0));
|
|
5987
|
+
const noCommits = Math.max(
|
|
5988
|
+
0,
|
|
5989
|
+
Number(task?.consecutiveNoCommits || task?.meta?.consecutiveNoCommits || 0),
|
|
5990
|
+
);
|
|
5991
|
+
const blockedReason = String(task?.blockedReason || task?.meta?.blockedReason || "").trim();
|
|
5992
|
+
const debtTrendSignal = resolvePlannerDebtTrendSignal(task);
|
|
5993
|
+
const commitEvidence = hasTaskCommitEvidence(task);
|
|
5994
|
+
const status = String(task?.status || "").trim().toLowerCase();
|
|
5995
|
+
const completedStatus = ["done", "completed", "closed", "merged"].includes(status);
|
|
5996
|
+
const agentAttemptsPenalty = commitEvidence ? 0 : (attempts * weights.agentAttempts);
|
|
5997
|
+
const consecutiveNoCommitsPenalty = noCommits * weights.consecutiveNoCommits;
|
|
5998
|
+
const blockedPenalty = blockedReason ? weights.blockedReason : 0;
|
|
5999
|
+
const debtTrendPenalty = debtTrendSignal * weights.debtTrend;
|
|
6000
|
+
|
|
6001
|
+
const failureWeight =
|
|
6002
|
+
agentAttemptsPenalty +
|
|
6003
|
+
consecutiveNoCommitsPenalty +
|
|
6004
|
+
blockedPenalty +
|
|
6005
|
+
debtTrendPenalty;
|
|
6006
|
+
const successWeight =
|
|
6007
|
+
(commitEvidence ? weights.commitSuccess : 0) +
|
|
6008
|
+
((completedStatus && !blockedReason) ? weights.completedSuccess : 0);
|
|
6009
|
+
const commitlessFailureEvent = attempts > 0 && !commitEvidence;
|
|
6010
|
+
|
|
6011
|
+
return {
|
|
6012
|
+
attempts,
|
|
6013
|
+
noCommits,
|
|
6014
|
+
blockedReason,
|
|
6015
|
+
debtTrendSignal,
|
|
6016
|
+
commitEvidence,
|
|
6017
|
+
commitlessFailureEvent,
|
|
6018
|
+
failureWeight,
|
|
6019
|
+
successWeight,
|
|
6020
|
+
failureComponents: {
|
|
6021
|
+
agentAttemptsPenalty,
|
|
6022
|
+
consecutiveNoCommitsPenalty,
|
|
6023
|
+
blockedPenalty,
|
|
6024
|
+
debtTrendPenalty,
|
|
6025
|
+
},
|
|
6026
|
+
};
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
function resolvePlannerPriorStatePath() {
|
|
6030
|
+
const configured = String(process.env.BOSUN_PLANNER_PATTERN_PRIORS_FILE || "").trim();
|
|
6031
|
+
if (configured) return configured;
|
|
6032
|
+
return resolve(process.cwd(), ".bosun", "workflow-runs", "planner-pattern-priors.json");
|
|
6033
|
+
}
|
|
6034
|
+
|
|
6035
|
+
function shouldPersistPlannerPriorState() {
|
|
6036
|
+
if (String(process.env.BOSUN_DISABLE_PLANNER_PATTERN_PRIORS || "").trim().toLowerCase() === "true") {
|
|
6037
|
+
return false;
|
|
6038
|
+
}
|
|
6039
|
+
if (process.env.VITEST && process.env.BOSUN_TEST_ENABLE_PLANNER_PRIOR_PERSISTENCE !== "true") {
|
|
6040
|
+
return false;
|
|
6041
|
+
}
|
|
6042
|
+
return true;
|
|
6043
|
+
}
|
|
6044
|
+
|
|
6045
|
+
function loadPlannerPriorState(statePath) {
|
|
6046
|
+
const base = { version: 1, patterns: {}, outcomes: {} };
|
|
6047
|
+
if (!statePath || !existsSync(statePath)) return base;
|
|
6048
|
+
try {
|
|
6049
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf8"));
|
|
6050
|
+
if (!parsed || typeof parsed !== "object") return base;
|
|
6051
|
+
return {
|
|
6052
|
+
version: 1,
|
|
6053
|
+
patterns:
|
|
6054
|
+
parsed.patterns && typeof parsed.patterns === "object"
|
|
6055
|
+
? Object.fromEntries(
|
|
6056
|
+
Object.entries(parsed.patterns).map(([key, value]) => [
|
|
6057
|
+
key,
|
|
6058
|
+
normalizePlannerPatternPrior(value),
|
|
6059
|
+
]),
|
|
6060
|
+
)
|
|
6061
|
+
: {},
|
|
6062
|
+
outcomes: parsed.outcomes && typeof parsed.outcomes === "object" ? parsed.outcomes : {},
|
|
6063
|
+
};
|
|
6064
|
+
} catch {
|
|
6065
|
+
return base;
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
function savePlannerPriorState(statePath, state) {
|
|
6070
|
+
if (!statePath) return;
|
|
6071
|
+
try {
|
|
6072
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
6073
|
+
writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
6074
|
+
} catch {
|
|
6075
|
+
// Best-effort persistence only.
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
function replayPlannerOutcomes(existingTasks, priorState, weights) {
|
|
6080
|
+
if (!Array.isArray(existingTasks) || existingTasks.length === 0) return;
|
|
6081
|
+
const nowIso = new Date().toISOString();
|
|
6082
|
+
const maxOutcomes = 5000;
|
|
6083
|
+
|
|
6084
|
+
for (const task of existingTasks) {
|
|
6085
|
+
const taskId = String(task?.id || task?.task_id || "").trim();
|
|
6086
|
+
if (!taskId) continue;
|
|
6087
|
+
const keys = resolvePlannerPatternKeys(task);
|
|
6088
|
+
if (!keys.length) continue;
|
|
6089
|
+
const signals = resolvePlannerOutcomeSignals(task, weights);
|
|
6090
|
+
const signature = JSON.stringify({
|
|
6091
|
+
status: String(task?.status || "").trim().toLowerCase(),
|
|
6092
|
+
attempts: signals.attempts,
|
|
6093
|
+
noCommits: signals.noCommits,
|
|
6094
|
+
blockedReason: signals.blockedReason.toLowerCase(),
|
|
6095
|
+
debtTrendSignal: signals.debtTrendSignal,
|
|
6096
|
+
hasCommits: hasTaskCommitEvidence(task),
|
|
6097
|
+
});
|
|
6098
|
+
if (priorState.outcomes?.[taskId]?.signature === signature) continue;
|
|
6099
|
+
priorState.outcomes[taskId] = { signature, updatedAt: nowIso };
|
|
6100
|
+
|
|
6101
|
+
for (const key of keys) {
|
|
6102
|
+
const current = normalizePlannerPatternPrior(priorState.patterns[key]);
|
|
6103
|
+
const priorCounter = Math.max(0, Number(current.failureCounter || 0));
|
|
6104
|
+
const priorCommitlessCounter = Math.max(0, Number(current.commitlessFailureCounter || 0));
|
|
6105
|
+
if (signals.failureWeight > 0) {
|
|
6106
|
+
current.failureCount = Number(current.failureCount || 0) + 1;
|
|
6107
|
+
current.failureWeight = Number(current.failureWeight || 0) + signals.failureWeight;
|
|
6108
|
+
current.signalTotals.agentAttempts += signals.failureComponents.agentAttemptsPenalty;
|
|
6109
|
+
current.signalTotals.consecutiveNoCommits += signals.failureComponents.consecutiveNoCommitsPenalty;
|
|
6110
|
+
current.signalTotals.blockedReason += signals.failureComponents.blockedPenalty;
|
|
6111
|
+
current.signalTotals.debtTrend += signals.failureComponents.debtTrendPenalty;
|
|
6112
|
+
}
|
|
6113
|
+
if (signals.successWeight > 0) {
|
|
6114
|
+
current.successCount = Number(current.successCount || 0) + 1;
|
|
6115
|
+
current.successWeight = Number(current.successWeight || 0) + signals.successWeight;
|
|
6116
|
+
}
|
|
6117
|
+
if (signals.commitlessFailureEvent) {
|
|
6118
|
+
current.commitlessFailureCount = Number(current.commitlessFailureCount || 0) + 1;
|
|
6119
|
+
}
|
|
6120
|
+
if (signals.commitEvidence) {
|
|
6121
|
+
current.commitlessSuccessCount = Number(current.commitlessSuccessCount || 0) + 1;
|
|
6122
|
+
}
|
|
6123
|
+
current.failureCounter = Number(
|
|
6124
|
+
Math.max(
|
|
6125
|
+
0,
|
|
6126
|
+
(priorCounter * 0.82) + signals.failureWeight - (signals.successWeight * 0.95),
|
|
6127
|
+
).toFixed(3),
|
|
6128
|
+
);
|
|
6129
|
+
current.commitlessFailureCounter = Number(
|
|
6130
|
+
Math.max(
|
|
6131
|
+
0,
|
|
6132
|
+
(priorCommitlessCounter * 0.86) +
|
|
6133
|
+
(signals.commitlessFailureEvent ? 1.25 : 0) -
|
|
6134
|
+
(signals.commitEvidence ? 1.1 : 0),
|
|
6135
|
+
).toFixed(3),
|
|
6136
|
+
);
|
|
6137
|
+
current.lastUpdatedAt = nowIso;
|
|
6138
|
+
priorState.patterns[key] = current;
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
const outcomeEntries = Object.entries(priorState.outcomes || {});
|
|
6143
|
+
if (outcomeEntries.length > maxOutcomes) {
|
|
6144
|
+
outcomeEntries
|
|
6145
|
+
.sort((a, b) => String(a[1]?.updatedAt || "").localeCompare(String(b[1]?.updatedAt || "")))
|
|
6146
|
+
.slice(0, outcomeEntries.length - maxOutcomes)
|
|
6147
|
+
.forEach(([id]) => {
|
|
6148
|
+
delete priorState.outcomes[id];
|
|
6149
|
+
});
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6153
|
+
function rankPlannerTaskCandidates(tasks, priorState, rankingConfig) {
|
|
6154
|
+
const scored = (Array.isArray(tasks) ? tasks : []).map((task) => {
|
|
6155
|
+
const impact = Number.isFinite(task?.impact) ? Number(task.impact) : 5;
|
|
6156
|
+
const confidence = Number.isFinite(task?.confidence) ? Number(task.confidence) : 5;
|
|
6157
|
+
const riskLevel = String(task?.risk || "").trim().toLowerCase();
|
|
6158
|
+
const riskPenalty = ({ low: 0, medium: 0.4, high: 0.9, critical: 1.6 })[riskLevel] || 0;
|
|
6159
|
+
const baseScore = (impact * 1.15) + (confidence * 0.85) - riskPenalty;
|
|
6160
|
+
|
|
6161
|
+
const keys = resolvePlannerPatternKeys(task);
|
|
6162
|
+
const penalties = keys.map((key) => {
|
|
6163
|
+
const prior = priorState?.patterns?.[key];
|
|
6164
|
+
if (!prior || typeof prior !== "object") return { key, signalPenalty: 0, negativePrior: 0 };
|
|
6165
|
+
const failureCount = Number(prior.failureCount || 0);
|
|
6166
|
+
const successCount = Number(prior.successCount || 0);
|
|
6167
|
+
const failureWeight = Number(prior.failureWeight || 0);
|
|
6168
|
+
const successWeight = Number(prior.successWeight || 0);
|
|
6169
|
+
const failureCounter = Number(prior.failureCounter || 0);
|
|
6170
|
+
const commitlessFailureCounter = Number(prior.commitlessFailureCounter || 0);
|
|
6171
|
+
const commitlessFailureCount = Number(prior.commitlessFailureCount || 0);
|
|
6172
|
+
const commitlessSuccessCount = Number(prior.commitlessSuccessCount || 0);
|
|
6173
|
+
const netFailureEvents = Math.max(0, failureCount - successCount);
|
|
6174
|
+
const netFailureWeight = Math.max(0, failureWeight - successWeight);
|
|
6175
|
+
const netCommitlessEvents = Math.max(0, commitlessFailureCount - commitlessSuccessCount);
|
|
6176
|
+
const repeatedFailureSignal = Math.max(
|
|
6177
|
+
netFailureEvents,
|
|
6178
|
+
Math.max(0, failureCounter),
|
|
6179
|
+
netCommitlessEvents,
|
|
6180
|
+
Math.max(0, commitlessFailureCounter),
|
|
6181
|
+
);
|
|
6182
|
+
const signalPenalty = Math.max(
|
|
6183
|
+
netFailureWeight * rankingConfig.signalPenaltyScale,
|
|
6184
|
+
Math.max(0, failureCounter) * rankingConfig.signalPenaltyScale,
|
|
6185
|
+
);
|
|
6186
|
+
const negativePrior =
|
|
6187
|
+
repeatedFailureSignal >= rankingConfig.failureThreshold
|
|
6188
|
+
? Math.min(
|
|
6189
|
+
rankingConfig.maxNegativePrior,
|
|
6190
|
+
rankingConfig.failurePriorStep * (repeatedFailureSignal - rankingConfig.failureThreshold + 1),
|
|
6191
|
+
)
|
|
6192
|
+
: 0;
|
|
6193
|
+
return {
|
|
6194
|
+
key,
|
|
6195
|
+
signalPenalty,
|
|
6196
|
+
negativePrior,
|
|
6197
|
+
failureCounter: Math.max(0, failureCounter),
|
|
6198
|
+
commitlessFailureCounter: Math.max(0, commitlessFailureCounter),
|
|
6199
|
+
netCommitlessEvents,
|
|
6200
|
+
};
|
|
6201
|
+
});
|
|
6202
|
+
const totalPenalty = penalties.reduce(
|
|
6203
|
+
(sum, item) => sum + item.signalPenalty + item.negativePrior,
|
|
6204
|
+
0,
|
|
6205
|
+
);
|
|
6206
|
+
const averagePenalty = penalties.length > 0 ? totalPenalty / penalties.length : 0;
|
|
6207
|
+
const rankScore = baseScore - averagePenalty;
|
|
6208
|
+
|
|
6209
|
+
return {
|
|
6210
|
+
...task,
|
|
6211
|
+
_ranking: {
|
|
6212
|
+
baseScore: Number(baseScore.toFixed(3)),
|
|
6213
|
+
penalty: Number(averagePenalty.toFixed(3)),
|
|
6214
|
+
score: Number(rankScore.toFixed(3)),
|
|
6215
|
+
patternKeys: keys,
|
|
6216
|
+
penalties,
|
|
6217
|
+
},
|
|
6218
|
+
};
|
|
6219
|
+
});
|
|
6220
|
+
|
|
6221
|
+
scored.sort((a, b) => {
|
|
6222
|
+
if ((b?._ranking?.score || 0) !== (a?._ranking?.score || 0)) {
|
|
6223
|
+
return (b?._ranking?.score || 0) - (a?._ranking?.score || 0);
|
|
6224
|
+
}
|
|
6225
|
+
return Number(a?.index || 0) - Number(b?.index || 0);
|
|
6226
|
+
});
|
|
6227
|
+
return scored;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
4833
6230
|
function buildPlannerSkipReasonHistogram(skipped = []) {
|
|
4834
6231
|
const histogram = {};
|
|
4835
6232
|
for (const entry of skipped) {
|
|
@@ -4839,7 +6236,7 @@ function buildPlannerSkipReasonHistogram(skipped = []) {
|
|
|
4839
6236
|
return histogram;
|
|
4840
6237
|
}
|
|
4841
6238
|
|
|
4842
|
-
|
|
6239
|
+
registerBuiltinNodeType("action.materialize_planner_tasks", {
|
|
4843
6240
|
describe: () => "Parse planner JSON output and create backlog tasks in Kanban",
|
|
4844
6241
|
schema: {
|
|
4845
6242
|
type: "object",
|
|
@@ -4854,6 +6251,10 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
4854
6251
|
minImpactScore: { type: "number", default: CALIBRATED_MIN_IMPACT_SCORE, description: "Minimum planner impact score required for creation; accepts 0-1 or 0-10 scales" },
|
|
4855
6252
|
maxRiskWithoutHuman: { type: "string", default: CALIBRATED_MAX_RISK_WITHOUT_HUMAN, description: "Maximum planner risk level allowed for auto-creation (low|medium|high|critical)" },
|
|
4856
6253
|
maxConcurrentRepoAreaTasks: { type: "number", default: 0, description: "Maximum concurrent backlog tasks per repo area (0 disables limit)" },
|
|
6254
|
+
failurePriorThreshold: { type: "number", default: 2, description: "Net repeated failures required before applying negative priors" },
|
|
6255
|
+
failurePriorStep: { type: "number", default: 1.5, description: "Penalty added per repeated failure beyond threshold" },
|
|
6256
|
+
maxFailurePriorPenalty: { type: "number", default: 8, description: "Cap for repeated-failure negative prior penalty" },
|
|
6257
|
+
feedbackSignalScale: { type: "number", default: 0.12, description: "Scale factor applied to weighted feedback signal penalties" },
|
|
4857
6258
|
},
|
|
4858
6259
|
},
|
|
4859
6260
|
async execute(node, ctx, engine) {
|
|
@@ -4875,6 +6276,35 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
4875
6276
|
{ preferTenScaleIntegers: true },
|
|
4876
6277
|
) || CALIBRATED_MAX_RISK_WITHOUT_HUMAN;
|
|
4877
6278
|
const maxConcurrentRepoAreaTasks = Number(ctx.resolve(node.config?.maxConcurrentRepoAreaTasks ?? 0));
|
|
6279
|
+
const rankingConfig = {
|
|
6280
|
+
failureThreshold: Math.max(1, Number(ctx.resolve(node.config?.failurePriorThreshold ?? 2)) || 2),
|
|
6281
|
+
failurePriorStep: Math.max(0, Number(ctx.resolve(node.config?.failurePriorStep ?? 1.5)) || 1.5),
|
|
6282
|
+
maxNegativePrior: Math.max(0, Number(ctx.resolve(node.config?.maxFailurePriorPenalty ?? 8)) || 8),
|
|
6283
|
+
signalPenaltyScale: Math.max(0, Number(ctx.resolve(node.config?.feedbackSignalScale ?? 0.12)) || 0.12),
|
|
6284
|
+
};
|
|
6285
|
+
const plannerFeedback = resolvePlannerFeedbackObject(ctx.data?._plannerFeedback);
|
|
6286
|
+
const feedbackWeights = {
|
|
6287
|
+
agentAttempts: Math.max(
|
|
6288
|
+
0,
|
|
6289
|
+
Number(plannerFeedback?.rankingSignals?.weights?.agentAttempts || 0.6),
|
|
6290
|
+
),
|
|
6291
|
+
consecutiveNoCommits: Math.max(
|
|
6292
|
+
0,
|
|
6293
|
+
Number(
|
|
6294
|
+
plannerFeedback?.rankingSignals?.weights?.consecutiveNoCommits || 1.3,
|
|
6295
|
+
),
|
|
6296
|
+
),
|
|
6297
|
+
blockedReason: Math.max(
|
|
6298
|
+
0,
|
|
6299
|
+
Number(plannerFeedback?.rankingSignals?.weights?.blockedReason || 1.8),
|
|
6300
|
+
),
|
|
6301
|
+
debtTrend: Math.max(
|
|
6302
|
+
0,
|
|
6303
|
+
Number(plannerFeedback?.rankingSignals?.weights?.debtTrend || 0.7),
|
|
6304
|
+
),
|
|
6305
|
+
commitSuccess: 2.2,
|
|
6306
|
+
completedSuccess: 0.8,
|
|
6307
|
+
};
|
|
4878
6308
|
const materializationDefaults = resolvePlannerMaterializationDefaults(ctx);
|
|
4879
6309
|
|
|
4880
6310
|
const parsedTasks = extractPlannerTasksFromWorkflowOutput(outputText, maxTasks);
|
|
@@ -4904,14 +6334,19 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
4904
6334
|
|
|
4905
6335
|
const existingTitleSet = new Set();
|
|
4906
6336
|
const existingBacklogAreaCounts = new Map();
|
|
6337
|
+
let existingRows = [];
|
|
4907
6338
|
const shouldFetchExistingTasks =
|
|
4908
6339
|
Boolean(kanban?.listTasks)
|
|
4909
|
-
&& (
|
|
6340
|
+
&& (
|
|
6341
|
+
dedupEnabled
|
|
6342
|
+
|| (Number.isFinite(maxConcurrentRepoAreaTasks) && maxConcurrentRepoAreaTasks > 0)
|
|
6343
|
+
|| (Number.isFinite(rankingConfig.failureThreshold) && rankingConfig.failureThreshold > 0)
|
|
6344
|
+
);
|
|
4910
6345
|
if (shouldFetchExistingTasks) {
|
|
4911
6346
|
try {
|
|
4912
6347
|
const existing = await kanban.listTasks(projectId, {});
|
|
4913
|
-
|
|
4914
|
-
for (const row of
|
|
6348
|
+
existingRows = Array.isArray(existing) ? existing : [];
|
|
6349
|
+
for (const row of existingRows) {
|
|
4915
6350
|
const title = String(row?.title || "").trim().toLowerCase();
|
|
4916
6351
|
if (dedupEnabled && title) existingTitleSet.add(title);
|
|
4917
6352
|
const rowStatus = String(row?.status || "").trim().toLowerCase();
|
|
@@ -4928,12 +6363,79 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
4928
6363
|
ctx.log(node.id, `Could not prefetch tasks for dedup: ${err.message}`, "warn");
|
|
4929
6364
|
}
|
|
4930
6365
|
}
|
|
6366
|
+
const priorStatePath = shouldPersistPlannerPriorState()
|
|
6367
|
+
? resolvePlannerPriorStatePath()
|
|
6368
|
+
: "";
|
|
6369
|
+
const priorState = loadPlannerPriorState(priorStatePath);
|
|
6370
|
+
replayPlannerOutcomes(existingRows, priorState, feedbackWeights);
|
|
6371
|
+
const feedbackHotTasks = Array.isArray(plannerFeedback?.taskStore?.hotTasks)
|
|
6372
|
+
? plannerFeedback.taskStore.hotTasks
|
|
6373
|
+
: [];
|
|
6374
|
+
replayPlannerOutcomes(feedbackHotTasks, priorState, feedbackWeights);
|
|
6375
|
+
const feedbackPatterns = Array.isArray(plannerFeedback?.rankingSignals?.patterns)
|
|
6376
|
+
? plannerFeedback.rankingSignals.patterns
|
|
6377
|
+
: [];
|
|
6378
|
+
for (const pattern of feedbackPatterns) {
|
|
6379
|
+
if (!pattern || typeof pattern !== "object") continue;
|
|
6380
|
+
const key = String(
|
|
6381
|
+
pattern.key ||
|
|
6382
|
+
buildPlannerPatternKey(
|
|
6383
|
+
pattern.repoArea || pattern.repo_area || "global",
|
|
6384
|
+
pattern.archetype || "general",
|
|
6385
|
+
),
|
|
6386
|
+
).trim();
|
|
6387
|
+
if (!key) continue;
|
|
6388
|
+
const entry = normalizePlannerPatternPrior(priorState.patterns[key]);
|
|
6389
|
+
const incomingCounter = Math.max(0, Number(pattern.failureCounter || 0));
|
|
6390
|
+
const incomingFailures = Math.max(0, Number(pattern.failures || 0));
|
|
6391
|
+
const incomingSuccesses = Math.max(0, Number(pattern.successes || 0));
|
|
6392
|
+
const incomingCommitlessCounter = Math.max(
|
|
6393
|
+
0,
|
|
6394
|
+
Number(pattern.commitlessFailureCounter || pattern.commitless_counter || 0),
|
|
6395
|
+
);
|
|
6396
|
+
const incomingCommitlessFailures = Math.max(
|
|
6397
|
+
0,
|
|
6398
|
+
Number(pattern.commitlessFailures || pattern.commitless_failures || 0),
|
|
6399
|
+
);
|
|
6400
|
+
const incomingCommitlessSuccesses = Math.max(
|
|
6401
|
+
0,
|
|
6402
|
+
Number(pattern.commitlessSuccesses || pattern.commitless_successes || 0),
|
|
6403
|
+
);
|
|
6404
|
+
entry.failureCounter = Number(
|
|
6405
|
+
Math.max(entry.failureCounter || 0, incomingCounter).toFixed(3),
|
|
6406
|
+
);
|
|
6407
|
+
entry.failureCount = Math.max(
|
|
6408
|
+
Number(entry.failureCount || 0),
|
|
6409
|
+
incomingFailures,
|
|
6410
|
+
);
|
|
6411
|
+
entry.successCount = Math.max(
|
|
6412
|
+
Number(entry.successCount || 0),
|
|
6413
|
+
incomingSuccesses,
|
|
6414
|
+
);
|
|
6415
|
+
entry.commitlessFailureCounter = Number(
|
|
6416
|
+
Math.max(entry.commitlessFailureCounter || 0, incomingCommitlessCounter).toFixed(3),
|
|
6417
|
+
);
|
|
6418
|
+
entry.commitlessFailureCount = Math.max(
|
|
6419
|
+
Number(entry.commitlessFailureCount || 0),
|
|
6420
|
+
incomingCommitlessFailures,
|
|
6421
|
+
);
|
|
6422
|
+
entry.commitlessSuccessCount = Math.max(
|
|
6423
|
+
Number(entry.commitlessSuccessCount || 0),
|
|
6424
|
+
incomingCommitlessSuccesses,
|
|
6425
|
+
);
|
|
6426
|
+
entry.lastUpdatedAt = new Date().toISOString();
|
|
6427
|
+
priorState.patterns[key] = entry;
|
|
6428
|
+
}
|
|
6429
|
+
if (priorStatePath) {
|
|
6430
|
+
savePlannerPriorState(priorStatePath, priorState);
|
|
6431
|
+
}
|
|
6432
|
+
const rankedTasks = rankPlannerTaskCandidates(parsedTasks, priorState, rankingConfig);
|
|
4931
6433
|
|
|
4932
6434
|
const created = [];
|
|
4933
6435
|
const skipped = [];
|
|
4934
6436
|
const materializationOutcomes = [];
|
|
4935
6437
|
const createdAreaCounts = new Map();
|
|
4936
|
-
for (const task of
|
|
6438
|
+
for (const task of rankedTasks) {
|
|
4937
6439
|
const baseOutcome = {
|
|
4938
6440
|
title: task.title,
|
|
4939
6441
|
impact: task.impact,
|
|
@@ -5038,10 +6540,12 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
5038
6540
|
existingMeta.planner = {
|
|
5039
6541
|
nodeId: plannerNodeId,
|
|
5040
6542
|
index: task.index,
|
|
6543
|
+
archetype: task.archetype || null,
|
|
5041
6544
|
impact: task.impact,
|
|
5042
6545
|
confidence: task.confidence,
|
|
5043
6546
|
risk: task.risk,
|
|
5044
6547
|
estimated_effort: task.estimatedEffort,
|
|
6548
|
+
archetype: task.archetype,
|
|
5045
6549
|
repo_areas: task.repoAreas,
|
|
5046
6550
|
why_now: task.whyNow,
|
|
5047
6551
|
kill_criteria: task.killCriteria,
|
|
@@ -5087,10 +6591,17 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
5087
6591
|
created,
|
|
5088
6592
|
skipped,
|
|
5089
6593
|
tasks: parsedTasks,
|
|
6594
|
+
rankedTasks: rankedTasks.map((task) => ({
|
|
6595
|
+
title: task.title,
|
|
6596
|
+
archetype: task.archetype || null,
|
|
6597
|
+
score: task?._ranking?.score,
|
|
6598
|
+
penalty: task?._ranking?.penalty,
|
|
6599
|
+
patternKeys: task?._ranking?.patternKeys || [],
|
|
6600
|
+
})),
|
|
5090
6601
|
};
|
|
5091
6602
|
},
|
|
5092
6603
|
});
|
|
5093
|
-
|
|
6604
|
+
registerBuiltinNodeType("agent.run_planner", {
|
|
5094
6605
|
describe: () => "Run the task planner agent to generate new backlog tasks",
|
|
5095
6606
|
schema: {
|
|
5096
6607
|
type: "object",
|
|
@@ -5233,7 +6744,7 @@ registerNodeType("agent.run_planner", {
|
|
|
5233
6744
|
};
|
|
5234
6745
|
},
|
|
5235
6746
|
});
|
|
5236
|
-
|
|
6747
|
+
registerBuiltinNodeType("agent.evidence_collect", {
|
|
5237
6748
|
describe: () => "Collect all evidence from .bosun/evidence for review",
|
|
5238
6749
|
schema: {
|
|
5239
6750
|
type: "object",
|
|
@@ -5270,7 +6781,7 @@ registerNodeType("agent.evidence_collect", {
|
|
|
5270
6781
|
// FLOW CONTROL — Gates, barriers, and routing
|
|
5271
6782
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5272
6783
|
|
|
5273
|
-
|
|
6784
|
+
registerBuiltinNodeType("flow.gate", {
|
|
5274
6785
|
describe: () => "Pause workflow execution until a condition is met or manual approval is given",
|
|
5275
6786
|
schema: {
|
|
5276
6787
|
type: "object",
|
|
@@ -5341,7 +6852,7 @@ registerNodeType("flow.gate", {
|
|
|
5341
6852
|
},
|
|
5342
6853
|
});
|
|
5343
6854
|
|
|
5344
|
-
|
|
6855
|
+
registerBuiltinNodeType("flow.join", {
|
|
5345
6856
|
describe: () => "Explicitly join multiple branches before continuing",
|
|
5346
6857
|
schema: {
|
|
5347
6858
|
type: "object",
|
|
@@ -5440,7 +6951,7 @@ registerNodeType("flow.join", {
|
|
|
5440
6951
|
},
|
|
5441
6952
|
});
|
|
5442
6953
|
|
|
5443
|
-
|
|
6954
|
+
registerBuiltinNodeType("flow.end", {
|
|
5444
6955
|
describe: () => "End the workflow immediately with explicit terminal status",
|
|
5445
6956
|
schema: {
|
|
5446
6957
|
type: "object",
|
|
@@ -5596,14 +7107,14 @@ const UNIVERSAL_FLOW_NODE = {
|
|
|
5596
7107
|
},
|
|
5597
7108
|
};
|
|
5598
7109
|
|
|
5599
|
-
|
|
5600
|
-
|
|
7110
|
+
registerBuiltinNodeType("flow.universal", UNIVERSAL_FLOW_NODE);
|
|
7111
|
+
registerBuiltinNodeType("flow.universial", UNIVERSAL_FLOW_NODE);
|
|
5601
7112
|
|
|
5602
7113
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5603
7114
|
// LOOP / ITERATION
|
|
5604
7115
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5605
7116
|
|
|
5606
|
-
|
|
7117
|
+
registerBuiltinNodeType("loop.for_each", {
|
|
5607
7118
|
describe: () =>
|
|
5608
7119
|
"Iterate over an array, executing a sub-workflow for each item. " +
|
|
5609
7120
|
"Supports parallel fan-out via maxConcurrent and provides per-item " +
|
|
@@ -5696,7 +7207,7 @@ registerNodeType("loop.for_each", {
|
|
|
5696
7207
|
},
|
|
5697
7208
|
});
|
|
5698
7209
|
|
|
5699
|
-
|
|
7210
|
+
registerBuiltinNodeType("loop.while", {
|
|
5700
7211
|
describe: () =>
|
|
5701
7212
|
"Repeat a sub-workflow until a condition evaluates to false or max iterations " +
|
|
5702
7213
|
"are reached. Enables convergence loops (generate→verify→revise) by executing " +
|
|
@@ -5836,7 +7347,7 @@ registerNodeType("loop.while", {
|
|
|
5836
7347
|
// SESSION / AGENT MANAGEMENT — Direct session control
|
|
5837
7348
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5838
7349
|
|
|
5839
|
-
|
|
7350
|
+
registerBuiltinNodeType("action.continue_session", {
|
|
5840
7351
|
describe: () => "Re-attach to an existing agent session and send a continuation prompt",
|
|
5841
7352
|
schema: {
|
|
5842
7353
|
type: "object",
|
|
@@ -5894,7 +7405,7 @@ registerNodeType("action.continue_session", {
|
|
|
5894
7405
|
},
|
|
5895
7406
|
});
|
|
5896
7407
|
|
|
5897
|
-
|
|
7408
|
+
registerBuiltinNodeType("action.restart_agent", {
|
|
5898
7409
|
describe: () => "Kill and restart an agent session from scratch",
|
|
5899
7410
|
schema: {
|
|
5900
7411
|
type: "object",
|
|
@@ -5950,7 +7461,7 @@ registerNodeType("action.restart_agent", {
|
|
|
5950
7461
|
},
|
|
5951
7462
|
});
|
|
5952
7463
|
|
|
5953
|
-
|
|
7464
|
+
registerBuiltinNodeType("action.bosun_cli", {
|
|
5954
7465
|
describe: () => "Run a bosun CLI command (task, monitor, agent, etc.)",
|
|
5955
7466
|
schema: {
|
|
5956
7467
|
type: "object",
|
|
@@ -6020,7 +7531,7 @@ async function getKanbanMod() {
|
|
|
6020
7531
|
// input/output. Unlike action.bosun_cli (which shells out), this executes
|
|
6021
7532
|
// the tool script directly in-process and returns parsed, structured data.
|
|
6022
7533
|
|
|
6023
|
-
|
|
7534
|
+
registerBuiltinNodeType("action.bosun_tool", {
|
|
6024
7535
|
describe: () =>
|
|
6025
7536
|
"Invoke a Bosun built-in or custom tool programmatically. Returns " +
|
|
6026
7537
|
"structured output that downstream workflow nodes can consume via " +
|
|
@@ -6223,7 +7734,7 @@ registerNodeType("action.bosun_tool", {
|
|
|
6223
7734
|
// simpler ergonomics for the common case of "run workflow X and pipe
|
|
6224
7735
|
// its output to the next node".
|
|
6225
7736
|
|
|
6226
|
-
|
|
7737
|
+
registerBuiltinNodeType("action.invoke_workflow", {
|
|
6227
7738
|
describe: () =>
|
|
6228
7739
|
"Invoke another workflow and pipe its output to downstream nodes. " +
|
|
6229
7740
|
"Simpler than action.execute_workflow — designed for workflow-to-workflow " +
|
|
@@ -6649,7 +8160,7 @@ const BOSUN_FUNCTION_REGISTRY = Object.freeze({
|
|
|
6649
8160
|
},
|
|
6650
8161
|
});
|
|
6651
8162
|
|
|
6652
|
-
|
|
8163
|
+
registerBuiltinNodeType("action.bosun_function", {
|
|
6653
8164
|
describe: () =>
|
|
6654
8165
|
"Invoke an internal Bosun function directly (tasks, git, tools, workflows, config). " +
|
|
6655
8166
|
"Returns structured output that downstream nodes can consume. More powerful " +
|
|
@@ -6762,7 +8273,7 @@ registerNodeType("action.bosun_function", {
|
|
|
6762
8273
|
},
|
|
6763
8274
|
});
|
|
6764
8275
|
|
|
6765
|
-
|
|
8276
|
+
registerBuiltinNodeType("action.handle_rate_limit", {
|
|
6766
8277
|
describe: () => "Intelligently handle API rate limits with exponential backoff and provider rotation",
|
|
6767
8278
|
schema: {
|
|
6768
8279
|
type: "object",
|
|
@@ -6809,7 +8320,7 @@ registerNodeType("action.handle_rate_limit", {
|
|
|
6809
8320
|
},
|
|
6810
8321
|
});
|
|
6811
8322
|
|
|
6812
|
-
|
|
8323
|
+
registerBuiltinNodeType("action.ask_user", {
|
|
6813
8324
|
describe: () => "Pause workflow and ask the user for input via Telegram or UI",
|
|
6814
8325
|
schema: {
|
|
6815
8326
|
type: "object",
|
|
@@ -6855,7 +8366,7 @@ registerNodeType("action.ask_user", {
|
|
|
6855
8366
|
},
|
|
6856
8367
|
});
|
|
6857
8368
|
|
|
6858
|
-
|
|
8369
|
+
registerBuiltinNodeType("action.analyze_errors", {
|
|
6859
8370
|
describe: () => "Run the error detector on recent logs and classify failures",
|
|
6860
8371
|
schema: {
|
|
6861
8372
|
type: "object",
|
|
@@ -6917,7 +8428,7 @@ registerNodeType("action.analyze_errors", {
|
|
|
6917
8428
|
},
|
|
6918
8429
|
});
|
|
6919
8430
|
|
|
6920
|
-
|
|
8431
|
+
registerBuiltinNodeType("action.refresh_worktree", {
|
|
6921
8432
|
describe: () => "Refresh git worktree state — fetch, pull, or reset to clean state",
|
|
6922
8433
|
schema: {
|
|
6923
8434
|
type: "object",
|
|
@@ -7198,7 +8709,7 @@ async function _executeMcpToolCall(serverId, toolName, input, timeoutMs, ctx) {
|
|
|
7198
8709
|
};
|
|
7199
8710
|
}
|
|
7200
8711
|
|
|
7201
|
-
|
|
8712
|
+
registerBuiltinNodeType("action.mcp_tool_call", {
|
|
7202
8713
|
describe: () =>
|
|
7203
8714
|
"Call a tool on an installed MCP server with structured output extraction. " +
|
|
7204
8715
|
"Supports field extraction, output mapping, type coercion, and port-based " +
|
|
@@ -7350,7 +8861,7 @@ registerNodeType("action.mcp_tool_call", {
|
|
|
7350
8861
|
},
|
|
7351
8862
|
});
|
|
7352
8863
|
|
|
7353
|
-
|
|
8864
|
+
registerBuiltinNodeType("action.mcp_list_tools", {
|
|
7354
8865
|
describe: () =>
|
|
7355
8866
|
"List available tools on an installed MCP server, including their input " +
|
|
7356
8867
|
"schemas. Useful for dynamic tool discovery and auto-wiring in pipelines.",
|
|
@@ -7435,7 +8946,7 @@ registerNodeType("action.mcp_list_tools", {
|
|
|
7435
8946
|
|
|
7436
8947
|
// ── action.mcp_pipeline — Chain multiple MCP tool calls with data piping ──
|
|
7437
8948
|
|
|
7438
|
-
|
|
8949
|
+
registerBuiltinNodeType("action.mcp_pipeline", {
|
|
7439
8950
|
describe: () =>
|
|
7440
8951
|
"Execute a chain of MCP tool calls in sequence, piping structured output " +
|
|
7441
8952
|
"from each step to the next. Each step can extract specific fields from " +
|
|
@@ -7659,7 +9170,7 @@ registerNodeType("action.mcp_pipeline", {
|
|
|
7659
9170
|
|
|
7660
9171
|
// ── transform.mcp_extract — Extract structured data from any MCP output ──
|
|
7661
9172
|
|
|
7662
|
-
|
|
9173
|
+
registerBuiltinNodeType("transform.mcp_extract", {
|
|
7663
9174
|
describe: () =>
|
|
7664
9175
|
"Extract and reshape structured data from an upstream MCP tool call or " +
|
|
7665
9176
|
"any node output. Supports dot-path fields, JSON pointers, array wildcards, " +
|
|
@@ -8112,14 +9623,76 @@ function isValidGitWorktreePath(worktreePath) {
|
|
|
8112
9623
|
timeout: 5000,
|
|
8113
9624
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8114
9625
|
}).trim().toLowerCase();
|
|
8115
|
-
|
|
9626
|
+
if (inside !== "true") return false;
|
|
9627
|
+
// A nested folder inside the main repo also returns inside-work-tree=true.
|
|
9628
|
+
// Reuse is safe only when the path itself is the git top-level root.
|
|
9629
|
+
const topLevel = execGitArgsSync(["rev-parse", "--show-toplevel"], {
|
|
9630
|
+
cwd: worktreePath,
|
|
9631
|
+
encoding: "utf8",
|
|
9632
|
+
timeout: 5000,
|
|
9633
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9634
|
+
}).trim();
|
|
9635
|
+
const normalize = (value) =>
|
|
9636
|
+
resolve(String(value || ""))
|
|
9637
|
+
.replace(/\\/g, "/")
|
|
9638
|
+
.replace(/\/+$/, "");
|
|
9639
|
+
return normalize(topLevel) === normalize(worktreePath);
|
|
8116
9640
|
} catch {
|
|
8117
9641
|
return false;
|
|
8118
9642
|
}
|
|
8119
9643
|
}
|
|
8120
9644
|
|
|
9645
|
+
function resolveGitDirForWorktree(worktreePath) {
|
|
9646
|
+
if (!worktreePath || !existsSync(worktreePath)) return "";
|
|
9647
|
+
try {
|
|
9648
|
+
const topLevel = execGitArgsSync(["rev-parse", "--show-toplevel"], {
|
|
9649
|
+
cwd: worktreePath,
|
|
9650
|
+
encoding: "utf8",
|
|
9651
|
+
timeout: 5000,
|
|
9652
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9653
|
+
}).trim();
|
|
9654
|
+
const normalize = (value) =>
|
|
9655
|
+
resolve(String(value || ""))
|
|
9656
|
+
.replace(/\\/g, "/")
|
|
9657
|
+
.replace(/\/+$/, "")
|
|
9658
|
+
.toLowerCase();
|
|
9659
|
+
if (normalize(topLevel) !== normalize(worktreePath)) return "";
|
|
9660
|
+
const gitDir = execGitArgsSync(["rev-parse", "--git-dir"], {
|
|
9661
|
+
cwd: worktreePath,
|
|
9662
|
+
encoding: "utf8",
|
|
9663
|
+
timeout: 5000,
|
|
9664
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9665
|
+
}).trim();
|
|
9666
|
+
if (!gitDir) return "";
|
|
9667
|
+
return resolve(worktreePath, gitDir);
|
|
9668
|
+
} catch {
|
|
9669
|
+
return "";
|
|
9670
|
+
}
|
|
9671
|
+
}
|
|
9672
|
+
|
|
9673
|
+
function hasUnresolvedGitOperation(worktreePath) {
|
|
9674
|
+
if (!worktreePath || !existsSync(worktreePath)) return false;
|
|
9675
|
+
try {
|
|
9676
|
+
const gitDir = resolveGitDirForWorktree(worktreePath);
|
|
9677
|
+
if (!gitDir || !existsSync(gitDir)) return true;
|
|
9678
|
+
for (const marker of ["rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
|
|
9679
|
+
if (existsSync(resolve(gitDir, marker))) return true;
|
|
9680
|
+
}
|
|
9681
|
+
const unmerged = execGitArgsSync(["diff", "--name-only", "--diff-filter=U"], {
|
|
9682
|
+
cwd: worktreePath,
|
|
9683
|
+
encoding: "utf8",
|
|
9684
|
+
timeout: 5000,
|
|
9685
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9686
|
+
}).trim();
|
|
9687
|
+
return Boolean(unmerged);
|
|
9688
|
+
} catch {
|
|
9689
|
+
return true;
|
|
9690
|
+
}
|
|
9691
|
+
}
|
|
9692
|
+
|
|
8121
9693
|
function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
8122
9694
|
if (!worktreePath) return;
|
|
9695
|
+
const linkedGitDir = resolveGitDirForWorktree(worktreePath);
|
|
8123
9696
|
try {
|
|
8124
9697
|
execGitArgsSync(["worktree", "remove", String(worktreePath), "--force"], {
|
|
8125
9698
|
cwd: repoRoot,
|
|
@@ -8135,6 +9708,13 @@ function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
|
8135
9708
|
} catch {
|
|
8136
9709
|
/* best-effort */
|
|
8137
9710
|
}
|
|
9711
|
+
try {
|
|
9712
|
+
if (linkedGitDir && existsSync(linkedGitDir)) {
|
|
9713
|
+
rmSync(linkedGitDir, { recursive: true, force: true });
|
|
9714
|
+
}
|
|
9715
|
+
} catch {
|
|
9716
|
+
/* best-effort */
|
|
9717
|
+
}
|
|
8138
9718
|
try {
|
|
8139
9719
|
execGitArgsSync(["worktree", "prune"], {
|
|
8140
9720
|
cwd: repoRoot,
|
|
@@ -8163,7 +9743,7 @@ const STRICT_START_GUARD_MISSING_TASK = /^(1|true|yes|on)$/i.test(
|
|
|
8163
9743
|
|
|
8164
9744
|
// ── trigger.task_available ──────────────────────────────────────────────────
|
|
8165
9745
|
|
|
8166
|
-
|
|
9746
|
+
registerBuiltinNodeType("trigger.task_available", {
|
|
8167
9747
|
describe: () =>
|
|
8168
9748
|
"Polling trigger that fires when todo tasks are available. Handles " +
|
|
8169
9749
|
"slot limits, anti-thrash filtering, cooldowns, task sorting (fire " +
|
|
@@ -8544,7 +10124,7 @@ registerNodeType("trigger.task_available", {
|
|
|
8544
10124
|
});
|
|
8545
10125
|
// ── condition.slot_available ────────────────────────────────────────────────
|
|
8546
10126
|
|
|
8547
|
-
|
|
10127
|
+
registerBuiltinNodeType("condition.slot_available", {
|
|
8548
10128
|
describe: () =>
|
|
8549
10129
|
"Gate checking both global and per-base-branch concurrency limits.",
|
|
8550
10130
|
schema: {
|
|
@@ -8579,7 +10159,7 @@ registerNodeType("condition.slot_available", {
|
|
|
8579
10159
|
|
|
8580
10160
|
// ── action.allocate_slot ────────────────────────────────────────────────────
|
|
8581
10161
|
|
|
8582
|
-
|
|
10162
|
+
registerBuiltinNodeType("action.allocate_slot", {
|
|
8583
10163
|
describe: () =>
|
|
8584
10164
|
"Reserve a parallel execution slot. Saves process env snapshot for " +
|
|
8585
10165
|
"parallel isolation and stores slot metadata in workflow context.",
|
|
@@ -8637,7 +10217,7 @@ registerNodeType("action.allocate_slot", {
|
|
|
8637
10217
|
|
|
8638
10218
|
// ── action.release_slot ─────────────────────────────────────────────────────
|
|
8639
10219
|
|
|
8640
|
-
|
|
10220
|
+
registerBuiltinNodeType("action.release_slot", {
|
|
8641
10221
|
describe: () =>
|
|
8642
10222
|
"Release a previously allocated execution slot. Restores saved env vars " +
|
|
8643
10223
|
"for parallel isolation. Idempotent — safe on double-call.",
|
|
@@ -8672,7 +10252,7 @@ registerNodeType("action.release_slot", {
|
|
|
8672
10252
|
|
|
8673
10253
|
// ── action.claim_task ───────────────────────────────────────────────────────
|
|
8674
10254
|
|
|
8675
|
-
|
|
10255
|
+
registerBuiltinNodeType("action.claim_task", {
|
|
8676
10256
|
describe: () =>
|
|
8677
10257
|
"Acquire a distributed task claim with auto-renewal. Prevents duplicate " +
|
|
8678
10258
|
"execution across orchestrators. Stores claim token + renewal timer in " +
|
|
@@ -8802,7 +10382,7 @@ registerNodeType("action.claim_task", {
|
|
|
8802
10382
|
|
|
8803
10383
|
// ── action.release_claim ────────────────────────────────────────────────────
|
|
8804
10384
|
|
|
8805
|
-
|
|
10385
|
+
registerBuiltinNodeType("action.release_claim", {
|
|
8806
10386
|
describe: () =>
|
|
8807
10387
|
"Release a distributed task claim + cancel renewal timer. Idempotent.",
|
|
8808
10388
|
schema: {
|
|
@@ -8866,7 +10446,7 @@ registerNodeType("action.release_claim", {
|
|
|
8866
10446
|
|
|
8867
10447
|
// ── action.resolve_executor ─────────────────────────────────────────────────
|
|
8868
10448
|
|
|
8869
|
-
|
|
10449
|
+
registerBuiltinNodeType("action.resolve_executor", {
|
|
8870
10450
|
describe: () =>
|
|
8871
10451
|
"Pick SDK + model via complexity routing, env overrides, or defaults.",
|
|
8872
10452
|
schema: {
|
|
@@ -8893,6 +10473,12 @@ registerNodeType("action.resolve_executor", {
|
|
|
8893
10473
|
description: cfgOrCtx(node, ctx, "taskDescription"),
|
|
8894
10474
|
tags: Array.isArray(ctx.data?.task?.tags) ? ctx.data.task.tags : [],
|
|
8895
10475
|
};
|
|
10476
|
+
const requestedAgentProfileId = String(
|
|
10477
|
+
cfgOrCtx(node, ctx, "agentProfile")
|
|
10478
|
+
|| ctx.data?.task?.agentProfile
|
|
10479
|
+
|| ctx.data?.agentProfile
|
|
10480
|
+
|| "",
|
|
10481
|
+
).trim();
|
|
8896
10482
|
let profileDecision = null;
|
|
8897
10483
|
let configuredExecutorPreference = null;
|
|
8898
10484
|
|
|
@@ -8917,12 +10503,32 @@ registerNodeType("action.resolve_executor", {
|
|
|
8917
10503
|
title: task.title,
|
|
8918
10504
|
description: task.description,
|
|
8919
10505
|
tags: task.tags,
|
|
10506
|
+
agentType: ctx.data?.task?.agentType || ctx.data?.agentType || "",
|
|
8920
10507
|
repoRoot,
|
|
8921
10508
|
},
|
|
8922
|
-
{ topN:
|
|
10509
|
+
{ topN: Math.max(10, requestedAgentProfileId ? 25 : 10) },
|
|
8923
10510
|
);
|
|
8924
|
-
const
|
|
8925
|
-
const
|
|
10511
|
+
const candidates = Array.isArray(match?.candidates) ? match.candidates : [];
|
|
10512
|
+
const bestCandidate = match?.best || null;
|
|
10513
|
+
const autoMinScore = Number(match?.auto?.thresholds?.minScore || 12);
|
|
10514
|
+
const scoreQualified = Number(bestCandidate?.score || 0) >= autoMinScore;
|
|
10515
|
+
const matchedCandidate = requestedAgentProfileId
|
|
10516
|
+
? candidates.find((candidate) => String(candidate?.id || "").trim() === requestedAgentProfileId) || null
|
|
10517
|
+
: ((match?.auto?.shouldAutoApply || scoreQualified) ? bestCandidate : null);
|
|
10518
|
+
if (!requestedAgentProfileId && bestCandidate && !match?.auto?.shouldAutoApply) {
|
|
10519
|
+
ctx.log(
|
|
10520
|
+
node.id,
|
|
10521
|
+
`Profile match below auto threshold; ignoring candidate ${String(bestCandidate.id || "unknown")}`,
|
|
10522
|
+
);
|
|
10523
|
+
}
|
|
10524
|
+
const profile = matchedCandidate?.profile || null;
|
|
10525
|
+
const profileId = String(matchedCandidate?.id || "").trim();
|
|
10526
|
+
if (requestedAgentProfileId && !profileId) {
|
|
10527
|
+
ctx.log(
|
|
10528
|
+
node.id,
|
|
10529
|
+
`Requested agent profile "${requestedAgentProfileId}" not found; falling back to executor defaults`,
|
|
10530
|
+
);
|
|
10531
|
+
}
|
|
8926
10532
|
if (profileId && profile) {
|
|
8927
10533
|
profileDecision = { id: profileId, profile };
|
|
8928
10534
|
ctx.data.agentProfile = profileId;
|
|
@@ -9049,7 +10655,7 @@ registerNodeType("action.resolve_executor", {
|
|
|
9049
10655
|
|
|
9050
10656
|
// ── action.acquire_worktree ─────────────────────────────────────────────────
|
|
9051
10657
|
|
|
9052
|
-
|
|
10658
|
+
registerBuiltinNodeType("action.acquire_worktree", {
|
|
9053
10659
|
describe: () =>
|
|
9054
10660
|
"Create or checkout a git worktree for isolated task execution. " +
|
|
9055
10661
|
"Fetches base branch, creates worktree, handles branch conflicts.",
|
|
@@ -9126,6 +10732,9 @@ registerNodeType("action.acquire_worktree", {
|
|
|
9126
10732
|
if (!isValidGitWorktreePath(worktreePath)) {
|
|
9127
10733
|
ctx.log(node.id, `Managed worktree is invalid, recreating: ${worktreePath}`);
|
|
9128
10734
|
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
10735
|
+
} else if (hasUnresolvedGitOperation(worktreePath)) {
|
|
10736
|
+
ctx.log(node.id, `Managed worktree has unresolved git state, recreating: ${worktreePath}`);
|
|
10737
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
9129
10738
|
}
|
|
9130
10739
|
}
|
|
9131
10740
|
|
|
@@ -9141,14 +10750,20 @@ registerNodeType("action.acquire_worktree", {
|
|
|
9141
10750
|
} catch {
|
|
9142
10751
|
/* rebase failures are non-fatal for reuse */
|
|
9143
10752
|
}
|
|
10753
|
+
if (existsSync(worktreePath) && hasUnresolvedGitOperation(worktreePath)) {
|
|
10754
|
+
ctx.log(node.id, `Managed worktree refresh left unresolved git state, recreating: ${worktreePath}`);
|
|
10755
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
10756
|
+
}
|
|
10757
|
+
}
|
|
10758
|
+
if (existsSync(worktreePath)) {
|
|
10759
|
+
ctx.data.worktreePath = worktreePath;
|
|
10760
|
+
ctx.data._worktreeCreated = false;
|
|
10761
|
+
ctx.data._worktreeManaged = true;
|
|
10762
|
+
ctx.log(node.id, `Reusing worktree: ${worktreePath}`);
|
|
10763
|
+
const cleared1 = clearBlockedWorktreeIdentity(worktreePath);
|
|
10764
|
+
if (cleared1) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
|
|
10765
|
+
return { success: true, worktreePath, created: false, reused: true, branch, baseBranch };
|
|
9144
10766
|
}
|
|
9145
|
-
ctx.data.worktreePath = worktreePath;
|
|
9146
|
-
ctx.data._worktreeCreated = false;
|
|
9147
|
-
ctx.data._worktreeManaged = true;
|
|
9148
|
-
ctx.log(node.id, `Reusing worktree: ${worktreePath}`);
|
|
9149
|
-
const cleared1 = clearBlockedWorktreeIdentity(worktreePath);
|
|
9150
|
-
if (cleared1) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
|
|
9151
|
-
return { success: true, worktreePath, created: false, reused: true, branch, baseBranch };
|
|
9152
10767
|
}
|
|
9153
10768
|
|
|
9154
10769
|
// Create fresh worktree
|
|
@@ -9223,7 +10838,7 @@ registerNodeType("action.acquire_worktree", {
|
|
|
9223
10838
|
|
|
9224
10839
|
// ── action.release_worktree ─────────────────────────────────────────────────
|
|
9225
10840
|
|
|
9226
|
-
|
|
10841
|
+
registerBuiltinNodeType("action.release_worktree", {
|
|
9227
10842
|
describe: () =>
|
|
9228
10843
|
"Release a git worktree. Idempotent. Optionally prunes stale entries.",
|
|
9229
10844
|
schema: {
|
|
@@ -9434,7 +11049,7 @@ registerNodeType("action.workflow_contract_validation", workflowContractValidati
|
|
|
9434
11049
|
|
|
9435
11050
|
// ── action.build_task_prompt ────────────────────────────────────────────────
|
|
9436
11051
|
|
|
9437
|
-
|
|
11052
|
+
registerBuiltinNodeType("action.build_task_prompt", {
|
|
9438
11053
|
describe: () =>
|
|
9439
11054
|
"Compose the full agent prompt from task data, AGENTS.md, comments, " +
|
|
9440
11055
|
"copilot-instructions.md, agent status endpoint, and co-author trailer.",
|
|
@@ -9554,70 +11169,161 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9554
11169
|
const allowedRepositories = normalizeStringArray(repositories, primaryRepository);
|
|
9555
11170
|
const matchedSkills = findRelevantSkills(repoRoot, taskTitle, taskDescription || "", {});
|
|
9556
11171
|
const activeSkillFiles = matchedSkills.map((skill) => skill.filename);
|
|
11172
|
+
const strictCacheAnchoring =
|
|
11173
|
+
String(process.env.BOSUN_CACHE_ANCHOR_MODE || "")
|
|
11174
|
+
.trim()
|
|
11175
|
+
.toLowerCase() === "strict";
|
|
11176
|
+
|
|
11177
|
+
const buildStableSystemPrompt = () => {
|
|
11178
|
+
const systemParts = [];
|
|
11179
|
+
if (includeAgentsMd) {
|
|
11180
|
+
const searchDirs = [repoRoot].filter(Boolean);
|
|
11181
|
+
const docFiles = ["AGENTS.md", ".github/copilot-instructions.md"];
|
|
11182
|
+
const loaded = new Set();
|
|
11183
|
+
for (const dir of searchDirs) {
|
|
11184
|
+
for (const doc of docFiles) {
|
|
11185
|
+
if (loaded.has(doc)) continue;
|
|
11186
|
+
const fullPath = resolve(dir, doc);
|
|
11187
|
+
try {
|
|
11188
|
+
if (!existsSync(fullPath)) continue;
|
|
11189
|
+
const content = readFileSync(fullPath, "utf8").trim();
|
|
11190
|
+
if (!content || content.length <= 10) continue;
|
|
11191
|
+
loaded.add(doc);
|
|
11192
|
+
systemParts.push(`## ${doc}`);
|
|
11193
|
+
systemParts.push(content);
|
|
11194
|
+
systemParts.push("");
|
|
11195
|
+
} catch {
|
|
11196
|
+
// best-effort only
|
|
11197
|
+
}
|
|
11198
|
+
}
|
|
11199
|
+
}
|
|
11200
|
+
}
|
|
11201
|
+
|
|
11202
|
+
if (includeStatusEndpoint) {
|
|
11203
|
+
const port = process.env.AGENT_ENDPOINT_PORT || process.env.BOSUN_AGENT_ENDPOINT_PORT || "";
|
|
11204
|
+
if (port) {
|
|
11205
|
+
systemParts.push("## Agent Status Endpoint");
|
|
11206
|
+
systemParts.push(`POST http://127.0.0.1:${port}/status — Report progress`);
|
|
11207
|
+
systemParts.push(`POST http://127.0.0.1:${port}/heartbeat — Heartbeat ping`);
|
|
11208
|
+
systemParts.push(`POST http://127.0.0.1:${port}/error — Report errors`);
|
|
11209
|
+
systemParts.push(`POST http://127.0.0.1:${port}/complete — Signal completion`);
|
|
11210
|
+
systemParts.push("");
|
|
11211
|
+
}
|
|
11212
|
+
}
|
|
11213
|
+
|
|
11214
|
+
systemParts.push("## Tool Discovery");
|
|
11215
|
+
systemParts.push(
|
|
11216
|
+
"Bosun uses a compact MCP discovery layer for external MCP servers and the custom tool library.",
|
|
11217
|
+
);
|
|
11218
|
+
systemParts.push(
|
|
11219
|
+
"Preferred flow: `search` -> `get_schema` -> `execute`.",
|
|
11220
|
+
);
|
|
11221
|
+
systemParts.push(
|
|
11222
|
+
"Only eager tools are preloaded below to keep context small. Use `call_discovered_tool` only as a direct fallback when orchestration code is unnecessary.",
|
|
11223
|
+
);
|
|
11224
|
+
systemParts.push("");
|
|
11225
|
+
|
|
11226
|
+
const eagerToolBlock = getToolsPromptBlock(repoRoot, {
|
|
11227
|
+
includeBuiltins: true,
|
|
11228
|
+
eagerOnly: true,
|
|
11229
|
+
discoveryMode: true,
|
|
11230
|
+
emitReflectHint: true,
|
|
11231
|
+
limit: 12,
|
|
11232
|
+
});
|
|
11233
|
+
if (eagerToolBlock) {
|
|
11234
|
+
systemParts.push(eagerToolBlock);
|
|
11235
|
+
systemParts.push("");
|
|
11236
|
+
}
|
|
11237
|
+
|
|
11238
|
+
systemParts.push("## Instructions");
|
|
11239
|
+
systemParts.push(
|
|
11240
|
+
"1. Follow the project instructions in AGENTS.md.\n" +
|
|
11241
|
+
"2. Use the discovery MCP tools for non-eager MCP/custom tools before assuming a capability is unavailable.\n" +
|
|
11242
|
+
"3. Implement the required changes.\n" +
|
|
11243
|
+
"4. Ensure tests pass and build is clean with 0 warnings.\n" +
|
|
11244
|
+
"5. Commit your changes using conventional commits.\n" +
|
|
11245
|
+
"6. Never ask for user input — you are autonomous.\n" +
|
|
11246
|
+
"7. Use all available tools to verify your work.",
|
|
11247
|
+
);
|
|
11248
|
+
systemParts.push("");
|
|
11249
|
+
systemParts.push("## Git Attribution");
|
|
11250
|
+
systemParts.push("Add this trailer to all commits:");
|
|
11251
|
+
systemParts.push("Co-authored-by: bosun[bot] <bosun@virtengine.com>");
|
|
11252
|
+
return systemParts.join("\n").trim();
|
|
11253
|
+
};
|
|
9557
11254
|
|
|
9558
11255
|
if (customTemplate) {
|
|
11256
|
+
const stableSystemPrompt = buildStableSystemPrompt();
|
|
9559
11257
|
ctx.data._taskPrompt = customTemplate;
|
|
11258
|
+
ctx.data._taskUserPrompt = customTemplate;
|
|
11259
|
+
ctx.data._taskSystemPrompt = stableSystemPrompt;
|
|
9560
11260
|
ctx.log(node.id, `Prompt from custom template (${customTemplate.length} chars)`);
|
|
9561
|
-
return {
|
|
11261
|
+
return {
|
|
11262
|
+
success: true,
|
|
11263
|
+
prompt: customTemplate,
|
|
11264
|
+
userPrompt: customTemplate,
|
|
11265
|
+
systemPrompt: stableSystemPrompt,
|
|
11266
|
+
source: "custom",
|
|
11267
|
+
};
|
|
9562
11268
|
}
|
|
9563
11269
|
|
|
9564
|
-
const
|
|
11270
|
+
const userParts = [];
|
|
9565
11271
|
|
|
9566
11272
|
// Header
|
|
9567
|
-
|
|
9568
|
-
if (taskId)
|
|
9569
|
-
|
|
11273
|
+
userParts.push(`# Task: ${taskTitle}`);
|
|
11274
|
+
if (taskId) userParts.push(`Task ID: ${taskId}`);
|
|
11275
|
+
userParts.push("");
|
|
9570
11276
|
|
|
9571
11277
|
// Retry context (if applicable)
|
|
9572
11278
|
if (retryReason) {
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
11279
|
+
userParts.push("## Retry Context");
|
|
11280
|
+
userParts.push(`Previous attempt failed: ${retryReason}`);
|
|
11281
|
+
userParts.push("Try a different approach this time.");
|
|
11282
|
+
userParts.push("");
|
|
9577
11283
|
}
|
|
9578
11284
|
|
|
9579
11285
|
// Description
|
|
9580
11286
|
if (taskDescription) {
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
11287
|
+
userParts.push("## Description");
|
|
11288
|
+
userParts.push(taskDescription);
|
|
11289
|
+
userParts.push("");
|
|
9584
11290
|
}
|
|
9585
11291
|
|
|
9586
11292
|
// Environment context
|
|
9587
|
-
|
|
11293
|
+
userParts.push("## Environment");
|
|
9588
11294
|
const envLines = [];
|
|
9589
11295
|
if (worktreePath) envLines.push(`- **Working Directory:** ${worktreePath}`);
|
|
9590
11296
|
if (branch) envLines.push(`- **Branch:** ${branch}`);
|
|
9591
11297
|
if (baseBranch) envLines.push(`- **Base Branch:** ${baseBranch}`);
|
|
9592
11298
|
if (repoSlug) envLines.push(`- **Repository:** ${repoSlug}`);
|
|
9593
11299
|
if (repoRoot) envLines.push(`- **Repo Root:** ${repoRoot}`);
|
|
9594
|
-
if (envLines.length)
|
|
9595
|
-
|
|
11300
|
+
if (envLines.length) userParts.push(envLines.join("\n"));
|
|
11301
|
+
userParts.push("");
|
|
9596
11302
|
|
|
9597
11303
|
// Workspace and repository scope guardrails.
|
|
9598
|
-
|
|
9599
|
-
if (workspace)
|
|
9600
|
-
if (primaryRepository)
|
|
11304
|
+
userParts.push("## Workspace Scope Contract");
|
|
11305
|
+
if (workspace) userParts.push(`- **Workspace:** ${workspace}`);
|
|
11306
|
+
if (primaryRepository) userParts.push(`- **Primary Repository:** ${primaryRepository}`);
|
|
9601
11307
|
if (allowedRepositories.length > 0) {
|
|
9602
|
-
|
|
11308
|
+
userParts.push("- **Allowed Repositories:**");
|
|
9603
11309
|
for (const allowedRepo of allowedRepositories) {
|
|
9604
|
-
|
|
11310
|
+
userParts.push(` - ${allowedRepo}`);
|
|
9605
11311
|
}
|
|
9606
11312
|
} else {
|
|
9607
|
-
|
|
11313
|
+
userParts.push("- **Allowed Repositories:** (not declared)");
|
|
9608
11314
|
}
|
|
9609
|
-
if (worktreePath)
|
|
9610
|
-
|
|
9611
|
-
|
|
11315
|
+
if (worktreePath) userParts.push(`- **Write Scope Root:** ${worktreePath}`);
|
|
11316
|
+
userParts.push("");
|
|
11317
|
+
userParts.push("Hard boundaries:");
|
|
9612
11318
|
if (worktreePath) {
|
|
9613
|
-
|
|
11319
|
+
userParts.push(`1. Modify files only inside \`${worktreePath}\`.`);
|
|
9614
11320
|
} else {
|
|
9615
|
-
|
|
11321
|
+
userParts.push("1. Modify files only inside the active repository working directory.");
|
|
9616
11322
|
}
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
11323
|
+
userParts.push("2. Modify code only in the allowed repositories listed above.");
|
|
11324
|
+
userParts.push("3. If required work depends on an unlisted repository, stop and report `blocked: cross-repo dependency`.");
|
|
11325
|
+
userParts.push("4. In completion notes, list every repository you touched and why.");
|
|
11326
|
+
userParts.push("");
|
|
9621
11327
|
|
|
9622
11328
|
let workflowContractPromptBlock = String(ctx.data?._workflowContractPromptBlock || "").trim();
|
|
9623
11329
|
if (!workflowContractPromptBlock) {
|
|
@@ -9636,8 +11342,8 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9636
11342
|
}
|
|
9637
11343
|
}
|
|
9638
11344
|
if (workflowContractPromptBlock) {
|
|
9639
|
-
|
|
9640
|
-
|
|
11345
|
+
userParts.push(workflowContractPromptBlock);
|
|
11346
|
+
userParts.push("");
|
|
9641
11347
|
}
|
|
9642
11348
|
|
|
9643
11349
|
// AGENTS.md + copilot-instructions.md
|
|
@@ -9654,9 +11360,9 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9654
11360
|
const content = readFileSync(fullPath, "utf8").trim();
|
|
9655
11361
|
if (content && content.length > 10) {
|
|
9656
11362
|
loaded.add(doc);
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
11363
|
+
userParts.push(`## ${doc}`);
|
|
11364
|
+
userParts.push(content);
|
|
11365
|
+
userParts.push("");
|
|
9660
11366
|
}
|
|
9661
11367
|
}
|
|
9662
11368
|
} catch { /* best-effort */ }
|
|
@@ -9668,12 +11374,12 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9668
11374
|
if (includeStatusEndpoint) {
|
|
9669
11375
|
const port = process.env.AGENT_ENDPOINT_PORT || process.env.BOSUN_AGENT_ENDPOINT_PORT || "";
|
|
9670
11376
|
if (port) {
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
11377
|
+
userParts.push("## Agent Status Endpoint");
|
|
11378
|
+
userParts.push(`POST http://127.0.0.1:${port}/status — Report progress`);
|
|
11379
|
+
userParts.push(`POST http://127.0.0.1:${port}/heartbeat — Heartbeat ping`);
|
|
11380
|
+
userParts.push(`POST http://127.0.0.1:${port}/error — Report errors`);
|
|
11381
|
+
userParts.push(`POST http://127.0.0.1:${port}/complete — Signal completion`);
|
|
11382
|
+
userParts.push("");
|
|
9677
11383
|
}
|
|
9678
11384
|
}
|
|
9679
11385
|
|
|
@@ -9684,8 +11390,8 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9684
11390
|
{},
|
|
9685
11391
|
);
|
|
9686
11392
|
if (relevantSkillsBlock) {
|
|
9687
|
-
|
|
9688
|
-
|
|
11393
|
+
userParts.push(relevantSkillsBlock);
|
|
11394
|
+
userParts.push("");
|
|
9689
11395
|
}
|
|
9690
11396
|
|
|
9691
11397
|
// Inject library-resolved skills from agent.select_profile.
|
|
@@ -9711,70 +11417,73 @@ registerNodeType("action.build_task_prompt", {
|
|
|
9711
11417
|
librarySkillParts.push("");
|
|
9712
11418
|
}
|
|
9713
11419
|
if (librarySkillParts.length > 0) {
|
|
9714
|
-
|
|
9715
|
-
|
|
11420
|
+
userParts.push("## Library Skills");
|
|
11421
|
+
userParts.push(...librarySkillParts);
|
|
9716
11422
|
}
|
|
9717
11423
|
} catch (err) {
|
|
9718
11424
|
ctx.log(node.id, `Library skill injection failed (non-fatal): ${err.message}`);
|
|
9719
11425
|
}
|
|
9720
11426
|
}
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
parts.push(
|
|
9724
|
-
"Bosun uses a compact MCP discovery layer for external MCP servers and the custom tool library.",
|
|
9725
|
-
);
|
|
9726
|
-
parts.push(
|
|
9727
|
-
"Preferred flow: `search` -> `get_schema` -> `execute`.",
|
|
9728
|
-
);
|
|
9729
|
-
parts.push(
|
|
9730
|
-
"Only eager tools are preloaded below to keep context small. Use `call_discovered_tool` only as a direct fallback when orchestration code is unnecessary.",
|
|
9731
|
-
);
|
|
9732
|
-
parts.push("");
|
|
9733
|
-
|
|
9734
|
-
const eagerToolBlock = getToolsPromptBlock(repoRoot, {
|
|
11427
|
+
// Skill-driven eager tools belong with task context to preserve cache anchoring.
|
|
11428
|
+
const taskScopedEagerTools = getToolsPromptBlock(repoRoot, {
|
|
9735
11429
|
activeSkills: activeSkillFiles,
|
|
9736
11430
|
includeBuiltins: true,
|
|
9737
11431
|
eagerOnly: true,
|
|
9738
11432
|
discoveryMode: true,
|
|
9739
|
-
emitReflectHint:
|
|
11433
|
+
emitReflectHint: false,
|
|
9740
11434
|
limit: 12,
|
|
9741
11435
|
});
|
|
9742
|
-
if (
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
}
|
|
9746
|
-
|
|
9747
|
-
// Instructions
|
|
9748
|
-
parts.push("## Instructions");
|
|
9749
|
-
parts.push(
|
|
9750
|
-
"1. Read and understand the task description above.\n" +
|
|
9751
|
-
"2. Follow the project instructions in AGENTS.md.\n" +
|
|
9752
|
-
"3. Respect the Workspace Scope Contract and never cross repository boundaries.\n" +
|
|
9753
|
-
"4. Load and apply the matched important skills already inlined above.\n" +
|
|
9754
|
-
"5. Use the discovery MCP tools for non-eager MCP/custom tools before assuming a capability is unavailable.\n" +
|
|
9755
|
-
"6. Implement the required changes.\n" +
|
|
9756
|
-
"7. Ensure tests pass and build is clean with 0 warnings.\n" +
|
|
9757
|
-
"8. Commit your changes using conventional commits.\n" +
|
|
9758
|
-
"9. Never ask for user input — you are autonomous.\n" +
|
|
9759
|
-
"10. Use all available tools to verify your work.",
|
|
9760
|
-
);
|
|
9761
|
-
parts.push("");
|
|
11436
|
+
if (taskScopedEagerTools) {
|
|
11437
|
+
userParts.push(taskScopedEagerTools);
|
|
11438
|
+
userParts.push("");
|
|
11439
|
+
}
|
|
9762
11440
|
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
11441
|
+
const userPrompt = userParts.join("\n").trim();
|
|
11442
|
+
const systemPrompt = buildStableSystemPrompt();
|
|
11443
|
+
|
|
11444
|
+
if (strictCacheAnchoring) {
|
|
11445
|
+
const dynamicMarkers = [
|
|
11446
|
+
taskId,
|
|
11447
|
+
taskTitle,
|
|
11448
|
+
taskDescription,
|
|
11449
|
+
retryReason,
|
|
11450
|
+
branch,
|
|
11451
|
+
baseBranch,
|
|
11452
|
+
worktreePath,
|
|
11453
|
+
]
|
|
11454
|
+
.map((value) => String(value || "").trim())
|
|
11455
|
+
.filter(Boolean);
|
|
11456
|
+
const leaked = dynamicMarkers.find((marker) => systemPrompt.includes(marker));
|
|
11457
|
+
if (leaked) {
|
|
11458
|
+
throw new Error(
|
|
11459
|
+
`BOSUN_CACHE_ANCHOR_MODE=strict violation: system prompt leaked task-specific marker "${leaked}"`,
|
|
11460
|
+
);
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
9767
11463
|
|
|
9768
|
-
|
|
9769
|
-
ctx.data.
|
|
9770
|
-
ctx.
|
|
9771
|
-
|
|
11464
|
+
ctx.data._taskPrompt = userPrompt;
|
|
11465
|
+
ctx.data._taskUserPrompt = userPrompt;
|
|
11466
|
+
ctx.data._taskSystemPrompt = systemPrompt;
|
|
11467
|
+
ctx.log(
|
|
11468
|
+
node.id,
|
|
11469
|
+
`Prompt built (user=${userPrompt.length} chars, system=${systemPrompt.length} chars, strict=${strictCacheAnchoring})`,
|
|
11470
|
+
);
|
|
11471
|
+
return {
|
|
11472
|
+
success: true,
|
|
11473
|
+
prompt: userPrompt,
|
|
11474
|
+
userPrompt,
|
|
11475
|
+
systemPrompt,
|
|
11476
|
+
source: "generated",
|
|
11477
|
+
length: userPrompt.length,
|
|
11478
|
+
systemLength: systemPrompt.length,
|
|
11479
|
+
cacheAnchorMode: strictCacheAnchoring ? "strict" : "default",
|
|
11480
|
+
};
|
|
9772
11481
|
},
|
|
9773
11482
|
});
|
|
9774
11483
|
|
|
9775
11484
|
// ── action.detect_new_commits ───────────────────────────────────────────────
|
|
9776
11485
|
|
|
9777
|
-
|
|
11486
|
+
registerBuiltinNodeType("action.detect_new_commits", {
|
|
9778
11487
|
describe: () =>
|
|
9779
11488
|
"Compare pre/post execution HEAD to detect new commits. Also checks " +
|
|
9780
11489
|
"for unpushed commits vs base and collects diff stats.",
|
|
@@ -9898,7 +11607,7 @@ registerNodeType("action.detect_new_commits", {
|
|
|
9898
11607
|
|
|
9899
11608
|
// ── action.push_branch ──────────────────────────────────────────────────────
|
|
9900
11609
|
|
|
9901
|
-
|
|
11610
|
+
registerBuiltinNodeType("action.push_branch", {
|
|
9902
11611
|
describe: () =>
|
|
9903
11612
|
"Push the current branch to the remote. Includes rebase-before-push, " +
|
|
9904
11613
|
"empty-diff guard, protected branch safety, and optional main-branch sync.",
|
|
@@ -9910,6 +11619,7 @@ registerNodeType("action.push_branch", {
|
|
|
9910
11619
|
baseBranch: { type: "string", description: "Base branch to rebase onto" },
|
|
9911
11620
|
remote: { type: "string", default: "origin", description: "Remote name" },
|
|
9912
11621
|
forceWithLease: { type: "boolean", default: true, description: "Use --force-with-lease" },
|
|
11622
|
+
skipHooks: { type: "boolean", default: true, description: "Skip git pre-push hooks (--no-verify)" },
|
|
9913
11623
|
rebaseBeforePush: { type: "boolean", default: true, description: "Rebase onto base before push" },
|
|
9914
11624
|
emptyDiffGuard: { type: "boolean", default: true, description: "Abort if no files changed vs base" },
|
|
9915
11625
|
syncMainForModuleBranch: { type: "boolean", default: false, description: "Also sync base with main" },
|
|
@@ -9928,6 +11638,7 @@ registerNodeType("action.push_branch", {
|
|
|
9928
11638
|
const baseBranch = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
|
|
9929
11639
|
const remote = node.config?.remote || "origin";
|
|
9930
11640
|
const forceWithLease = node.config?.forceWithLease !== false;
|
|
11641
|
+
const skipHooks = node.config?.skipHooks !== false;
|
|
9931
11642
|
const rebaseBeforePush = node.config?.rebaseBeforePush !== false;
|
|
9932
11643
|
const emptyDiffGuard = node.config?.emptyDiffGuard !== false;
|
|
9933
11644
|
const syncMain = node.config?.syncMainForModuleBranch === true;
|
|
@@ -10039,8 +11750,10 @@ registerNodeType("action.push_branch", {
|
|
|
10039
11750
|
}
|
|
10040
11751
|
|
|
10041
11752
|
// ── Push ──
|
|
10042
|
-
const pushFlags =
|
|
10043
|
-
|
|
11753
|
+
const pushFlags = [];
|
|
11754
|
+
if (forceWithLease) pushFlags.push("--force-with-lease");
|
|
11755
|
+
if (skipHooks) pushFlags.push("--no-verify");
|
|
11756
|
+
const cmd = `git push ${pushFlags.join(" ")} --set-upstream ${remote} HEAD`.trim();
|
|
10044
11757
|
|
|
10045
11758
|
try {
|
|
10046
11759
|
const output = execSync(cmd, {
|
|
@@ -10072,7 +11785,7 @@ registerNodeType("action.push_branch", {
|
|
|
10072
11785
|
// WEB SEARCH — Structured web search for research workflows
|
|
10073
11786
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10074
11787
|
|
|
10075
|
-
|
|
11788
|
+
registerBuiltinNodeType("action.web_search", {
|
|
10076
11789
|
describe: () =>
|
|
10077
11790
|
"Perform a structured web search query and return results. Useful for " +
|
|
10078
11791
|
"research workflows (e.g., Aletheia-style math/science agents) that need " +
|
|
@@ -10255,5 +11968,24 @@ registerNodeType("action.web_search", {
|
|
|
10255
11968
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10256
11969
|
|
|
10257
11970
|
export { registerNodeType, getNodeType, listNodeTypes } from "./workflow-engine.mjs";
|
|
10258
|
-
|
|
10259
11971
|
export { evaluateTaskAssignedTriggerConfig };
|
|
11972
|
+
export {
|
|
11973
|
+
CUSTOM_NODE_DIR_NAME,
|
|
11974
|
+
getCustomNodeDir,
|
|
11975
|
+
scaffoldCustomNodeFile,
|
|
11976
|
+
startCustomNodeDiscovery,
|
|
11977
|
+
stopCustomNodeDiscovery,
|
|
11978
|
+
unregisterNodeType,
|
|
11979
|
+
};
|
|
11980
|
+
|
|
11981
|
+
export async function ensureWorkflowNodeTypesLoaded(options = {}) {
|
|
11982
|
+
if (!customLoadPromise || options.forceReload) {
|
|
11983
|
+
customLoadPromise = ensureCustomWorkflowNodesLoaded(options);
|
|
11984
|
+
}
|
|
11985
|
+
await customLoadPromise;
|
|
11986
|
+
if (!customDiscoveryStarted) {
|
|
11987
|
+
startCustomNodeDiscovery(options);
|
|
11988
|
+
customDiscoveryStarted = true;
|
|
11989
|
+
}
|
|
11990
|
+
return listNodeTypes();
|
|
11991
|
+
}
|