@diff-review-system/drs 4.0.1 → 4.1.0
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/.pi/agents/review/unified-reviewer.md +2 -6
- package/.pi/agents/task/review-assistant.md +22 -0
- package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +10 -4
- package/.pi/workflows/github-pr-review-post.yaml +0 -1
- package/.pi/workflows/github-pr-review.yaml +14 -8
- package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
- package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +10 -4
- package/.pi/workflows/gitlab-mr-review.yaml +14 -8
- package/.pi/workflows/gitlab-mr-visual-explain.yaml +46 -1
- package/.pi/workflows/local-fix-review-issues.yaml +13 -6
- package/.pi/workflows/local-review.yaml +7 -1
- package/.pi/workflows/local-visual-explain.yaml +46 -1
- package/.pi/workflows/temporal-control-smoke.yaml +63 -0
- package/README.md +39 -0
- package/dist/cli/index.js +211 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +6 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -10
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run-agent.d.ts +2 -7
- package/dist/cli/run-agent.d.ts.map +1 -1
- package/dist/cli/run-agent.js.map +1 -1
- package/dist/cli/workflow-executor-selection.d.ts +3 -0
- package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
- package/dist/cli/workflow-executor-selection.js +15 -0
- package/dist/cli/workflow-executor-selection.js.map +1 -0
- package/dist/cli/workflow.d.ts +38 -42
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +190 -838
- package/dist/cli/workflow.js.map +1 -1
- package/dist/lib/agent-result.d.ts +8 -0
- package/dist/lib/agent-result.d.ts.map +1 -0
- package/dist/lib/agent-result.js +2 -0
- package/dist/lib/agent-result.js.map +1 -0
- package/dist/lib/config.d.ts +21 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/output-paths.d.ts +0 -1
- package/dist/lib/output-paths.d.ts.map +1 -1
- package/dist/lib/output-paths.js +0 -1
- package/dist/lib/output-paths.js.map +1 -1
- package/dist/lib/project-setup.d.ts +10 -0
- package/dist/lib/project-setup.d.ts.map +1 -0
- package/dist/lib/project-setup.js +25 -0
- package/dist/lib/project-setup.js.map +1 -0
- package/dist/lib/review-artifact-store.d.ts +16 -0
- package/dist/lib/review-artifact-store.d.ts.map +1 -0
- package/dist/lib/review-artifact-store.js +84 -0
- package/dist/lib/review-artifact-store.js.map +1 -0
- package/dist/lib/review-artifact.d.ts.map +1 -1
- package/dist/lib/review-artifact.js +1 -0
- package/dist/lib/review-artifact.js.map +1 -1
- package/dist/lib/review-core.d.ts +1 -0
- package/dist/lib/review-core.d.ts.map +1 -1
- package/dist/lib/review-core.js +11 -14
- package/dist/lib/review-core.js.map +1 -1
- package/dist/lib/review-parser.d.ts.map +1 -1
- package/dist/lib/review-parser.js +15 -12
- package/dist/lib/review-parser.js.map +1 -1
- package/dist/lib/skills.d.ts +16 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +140 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/workflow/artifact-store.d.ts +71 -0
- package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
- package/dist/lib/workflow/artifact-store.js +121 -0
- package/dist/lib/workflow/artifact-store.js.map +1 -0
- package/dist/lib/workflow/compiled-plan.d.ts +87 -0
- package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
- package/dist/lib/workflow/compiled-plan.js +62 -0
- package/dist/lib/workflow/compiled-plan.js.map +1 -0
- package/dist/lib/workflow/executor.d.ts +18 -0
- package/dist/lib/workflow/executor.d.ts.map +1 -0
- package/dist/lib/workflow/executor.js +2 -0
- package/dist/lib/workflow/executor.js.map +1 -0
- package/dist/lib/workflow/graph.d.ts +30 -0
- package/dist/lib/workflow/graph.d.ts.map +1 -0
- package/dist/lib/workflow/graph.js +139 -0
- package/dist/lib/workflow/graph.js.map +1 -0
- package/dist/lib/workflow/input.d.ts +4 -0
- package/dist/lib/workflow/input.d.ts.map +1 -0
- package/dist/lib/workflow/input.js +4 -0
- package/dist/lib/workflow/input.js.map +1 -0
- package/dist/lib/workflow/node-executor.d.ts +19 -0
- package/dist/lib/workflow/node-executor.d.ts.map +1 -0
- package/dist/lib/workflow/node-executor.js +2 -0
- package/dist/lib/workflow/node-executor.js.map +1 -0
- package/dist/lib/workflow/planning.d.ts +51 -0
- package/dist/lib/workflow/planning.d.ts.map +1 -0
- package/dist/lib/workflow/planning.js +814 -0
- package/dist/lib/workflow/planning.js.map +1 -0
- package/dist/lib/workflow/types.d.ts +66 -0
- package/dist/lib/workflow/types.d.ts.map +1 -0
- package/dist/lib/workflow/types.js +2 -0
- package/dist/lib/workflow/types.js.map +1 -0
- package/dist/lib/write-json-output.d.ts.map +1 -1
- package/dist/lib/write-json-output.js +3 -8
- package/dist/lib/write-json-output.js.map +1 -1
- package/dist/pi/sdk.d.ts.map +1 -1
- package/dist/pi/sdk.js +6 -2
- package/dist/pi/sdk.js.map +1 -1
- package/dist/runtime/built-in-paths.d.ts +1 -0
- package/dist/runtime/built-in-paths.d.ts.map +1 -1
- package/dist/runtime/built-in-paths.js +7 -0
- package/dist/runtime/built-in-paths.js.map +1 -1
- package/dist/temporal/activities.d.ts +26 -0
- package/dist/temporal/activities.d.ts.map +1 -0
- package/dist/temporal/activities.js +219 -0
- package/dist/temporal/activities.js.map +1 -0
- package/dist/temporal/config.d.ts +4 -0
- package/dist/temporal/config.d.ts.map +1 -0
- package/dist/temporal/config.js +36 -0
- package/dist/temporal/config.js.map +1 -0
- package/dist/temporal/executor.d.ts +7 -0
- package/dist/temporal/executor.d.ts.map +1 -0
- package/dist/temporal/executor.js +233 -0
- package/dist/temporal/executor.js.map +1 -0
- package/dist/temporal/retry-policy.d.ts +28 -0
- package/dist/temporal/retry-policy.d.ts.map +1 -0
- package/dist/temporal/retry-policy.js +56 -0
- package/dist/temporal/retry-policy.js.map +1 -0
- package/dist/temporal/types.d.ts +86 -0
- package/dist/temporal/types.d.ts.map +1 -0
- package/dist/temporal/types.js +2 -0
- package/dist/temporal/types.js.map +1 -0
- package/dist/temporal/worker.d.ts +3 -0
- package/dist/temporal/worker.d.ts.map +1 -0
- package/dist/temporal/worker.js +18 -0
- package/dist/temporal/worker.js.map +1 -0
- package/dist/temporal/workflow-id.d.ts +27 -0
- package/dist/temporal/workflow-id.d.ts.map +1 -0
- package/dist/temporal/workflow-id.js +53 -0
- package/dist/temporal/workflow-id.js.map +1 -0
- package/dist/temporal/workflows.d.ts +6 -0
- package/dist/temporal/workflows.d.ts.map +1 -0
- package/dist/temporal/workflows.js +342 -0
- package/dist/temporal/workflows.js.map +1 -0
- package/package.json +6 -1
package/dist/cli/workflow.js
CHANGED
|
@@ -25,6 +25,10 @@ import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifac
|
|
|
25
25
|
import { runAgent } from './run-agent.js';
|
|
26
26
|
import { TraceCollector } from '../lib/trace-collector.js';
|
|
27
27
|
import { renderTraceHtml } from '../lib/trace-html.js';
|
|
28
|
+
import { buildWorkflowGraph, formatWorkflowGraphMermaid, } from '../lib/workflow/graph.js';
|
|
29
|
+
import { computeActiveWorkflowNodes, createSkippedWorkflowNodeResult, findWorkflowSegmentIndex, getNodeKind, getNodeNeeds, getWorkflowExecutionOrder, getWorkflowExecutionWaves, getWorkflowNodeSkipReason, getWorkflowNodes, hasWorkflowControlNodes, normalizeWorkflowBooleanLike, renderTemplate, runControlWorkflowNode, splitWorkflowSegments, } from '../lib/workflow/planning.js';
|
|
30
|
+
import { getWorkflowInputConfigType } from '../lib/workflow/input.js';
|
|
31
|
+
export { compileWorkflowPlan, } from '../lib/workflow/compiled-plan.js';
|
|
28
32
|
function createWorkflowLock() {
|
|
29
33
|
return { current: Promise.resolve() };
|
|
30
34
|
}
|
|
@@ -80,425 +84,6 @@ function getWorkflowPlatformClient(executionContext, platform) {
|
|
|
80
84
|
executionContext.platformClients[platform] = client;
|
|
81
85
|
return client;
|
|
82
86
|
}
|
|
83
|
-
function getNodeNeeds(node) {
|
|
84
|
-
if (node.needs === undefined) {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
if (!Array.isArray(node.needs)) {
|
|
88
|
-
throw new Error('Workflow node "needs" must be an array of node ids.');
|
|
89
|
-
}
|
|
90
|
-
return node.needs;
|
|
91
|
-
}
|
|
92
|
-
function getControlTargets(node) {
|
|
93
|
-
const targets = [];
|
|
94
|
-
if (node.target)
|
|
95
|
-
targets.push(node.target);
|
|
96
|
-
if (node.exit)
|
|
97
|
-
targets.push(node.exit);
|
|
98
|
-
if (node.default)
|
|
99
|
-
targets.push(node.default);
|
|
100
|
-
if (node.cases)
|
|
101
|
-
targets.push(...Object.values(node.cases));
|
|
102
|
-
return targets;
|
|
103
|
-
}
|
|
104
|
-
function validateWorkflowControlTargets(nodes) {
|
|
105
|
-
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
106
|
-
for (const target of getControlTargets(node)) {
|
|
107
|
-
if (!nodes[target]) {
|
|
108
|
-
throw new Error(`Workflow node "${nodeId}" targets unknown node "${target}".`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function validateWorkflowControlRouteDirection(nodes, executionOrder) {
|
|
114
|
-
const segments = splitWorkflowSegments(nodes, executionOrder);
|
|
115
|
-
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
|
116
|
-
const segment = segments[segmentIndex];
|
|
117
|
-
if (segment.type !== 'control') {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const node = nodes[segment.nodeId];
|
|
121
|
-
if (!node || node.control === 'loop') {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
for (const target of getControlTargets(node)) {
|
|
125
|
-
const targetIndex = findWorkflowSegmentIndex(segments, target);
|
|
126
|
-
if (targetIndex <= segmentIndex) {
|
|
127
|
-
throw new Error(`Workflow control node "${segment.nodeId}" cannot jump backward to "${target}". Use control: loop with maxIterations for repeated execution.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function validateWorkflowPassThroughShape(nodeId, node) {
|
|
133
|
-
if (node.control !== 'passThrough') {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (!node.target) {
|
|
137
|
-
throw new Error(`Workflow passThrough node "${nodeId}" must define "target".`);
|
|
138
|
-
}
|
|
139
|
-
const forbiddenKeys = [
|
|
140
|
-
'if',
|
|
141
|
-
'then',
|
|
142
|
-
'else',
|
|
143
|
-
'exit',
|
|
144
|
-
'cases',
|
|
145
|
-
'default',
|
|
146
|
-
'maxIterations',
|
|
147
|
-
'onMaxIterations',
|
|
148
|
-
'value',
|
|
149
|
-
];
|
|
150
|
-
const extraFields = forbiddenKeys.filter((key) => key in node);
|
|
151
|
-
if (extraFields.length > 0) {
|
|
152
|
-
throw new Error(`Workflow passThrough node "${nodeId}" must not define extra control logic: ${extraFields.join(', ')}.`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const WORKFLOW_NODE_FIELDS = new Set([
|
|
156
|
-
'agent',
|
|
157
|
-
'agentsFrom',
|
|
158
|
-
'control',
|
|
159
|
-
'action',
|
|
160
|
-
'with',
|
|
161
|
-
'needs',
|
|
162
|
-
'if',
|
|
163
|
-
'target',
|
|
164
|
-
'exit',
|
|
165
|
-
'maxIterations',
|
|
166
|
-
'onMaxIterations',
|
|
167
|
-
'value',
|
|
168
|
-
'cases',
|
|
169
|
-
'default',
|
|
170
|
-
'input',
|
|
171
|
-
'output',
|
|
172
|
-
'writes',
|
|
173
|
-
'json',
|
|
174
|
-
]);
|
|
175
|
-
const EXECUTABLE_NODE_FIELDS = new Set([
|
|
176
|
-
'agent',
|
|
177
|
-
'agentsFrom',
|
|
178
|
-
'action',
|
|
179
|
-
'with',
|
|
180
|
-
'needs',
|
|
181
|
-
'if',
|
|
182
|
-
'input',
|
|
183
|
-
'output',
|
|
184
|
-
'writes',
|
|
185
|
-
'json',
|
|
186
|
-
]);
|
|
187
|
-
const CONTROL_NODE_FIELDS = {
|
|
188
|
-
loop: new Set([
|
|
189
|
-
'control',
|
|
190
|
-
'needs',
|
|
191
|
-
'if',
|
|
192
|
-
'target',
|
|
193
|
-
'exit',
|
|
194
|
-
'maxIterations',
|
|
195
|
-
'onMaxIterations',
|
|
196
|
-
'output',
|
|
197
|
-
]),
|
|
198
|
-
switch: new Set(['control', 'needs', 'value', 'cases', 'default', 'output']),
|
|
199
|
-
end: new Set(['control', 'needs', 'output']),
|
|
200
|
-
passThrough: new Set(['control', 'needs', 'target', 'output']),
|
|
201
|
-
};
|
|
202
|
-
const ACTION_OPTION_FIELDS = {
|
|
203
|
-
write: new Set(),
|
|
204
|
-
'git-diff': new Set(['staged']),
|
|
205
|
-
'git-add': new Set(['path', 'paths']),
|
|
206
|
-
'git-branch': new Set(['name', 'from', 'force']),
|
|
207
|
-
'git-commit': new Set(['message', 'path', 'paths']),
|
|
208
|
-
'git-push': new Set(['remote', 'branch', 'remoteBranch', 'setUpstream', 'force']),
|
|
209
|
-
'has-diff': new Set(['path', 'paths']),
|
|
210
|
-
'stack-guard': new Set(['source', 'allowStackedSource', 'reservedPrefixes']),
|
|
211
|
-
'review-threshold': new Set(['review', 'severity', 'minIssues']),
|
|
212
|
-
'save-artifact': new Set([
|
|
213
|
-
'kind',
|
|
214
|
-
'source',
|
|
215
|
-
'artifact',
|
|
216
|
-
'payload',
|
|
217
|
-
'platform',
|
|
218
|
-
'project',
|
|
219
|
-
'projectId',
|
|
220
|
-
'owner',
|
|
221
|
-
'repo',
|
|
222
|
-
'subject',
|
|
223
|
-
'changeKind',
|
|
224
|
-
'changeNumber',
|
|
225
|
-
'pr',
|
|
226
|
-
'mr',
|
|
227
|
-
'branch',
|
|
228
|
-
]),
|
|
229
|
-
'load-artifact': new Set([
|
|
230
|
-
'kind',
|
|
231
|
-
'source',
|
|
232
|
-
'id',
|
|
233
|
-
'platform',
|
|
234
|
-
'project',
|
|
235
|
-
'projectId',
|
|
236
|
-
'owner',
|
|
237
|
-
'repo',
|
|
238
|
-
'subject',
|
|
239
|
-
'changeKind',
|
|
240
|
-
'changeNumber',
|
|
241
|
-
'pr',
|
|
242
|
-
'mr',
|
|
243
|
-
'branch',
|
|
244
|
-
]),
|
|
245
|
-
'artifact-exists': new Set([
|
|
246
|
-
'kind',
|
|
247
|
-
'source',
|
|
248
|
-
'id',
|
|
249
|
-
'platform',
|
|
250
|
-
'project',
|
|
251
|
-
'projectId',
|
|
252
|
-
'owner',
|
|
253
|
-
'repo',
|
|
254
|
-
'subject',
|
|
255
|
-
'changeKind',
|
|
256
|
-
'changeNumber',
|
|
257
|
-
'pr',
|
|
258
|
-
'mr',
|
|
259
|
-
'branch',
|
|
260
|
-
]),
|
|
261
|
-
'create-review-artifact': new Set(['source', 'review']),
|
|
262
|
-
'review-artifact-status': new Set(['artifact']),
|
|
263
|
-
'review-artifact-add-finding': new Set(['artifact', 'issue', 'source']),
|
|
264
|
-
'review-artifact-update-findings': new Set([
|
|
265
|
-
'artifact',
|
|
266
|
-
'state',
|
|
267
|
-
'disposition',
|
|
268
|
-
'ids',
|
|
269
|
-
'fingerprints',
|
|
270
|
-
'severity',
|
|
271
|
-
]),
|
|
272
|
-
'review-artifact-promote-finding': new Set(['artifact', 'ids', 'fingerprints', 'severity']),
|
|
273
|
-
'review-artifact-resolve-finding': new Set(['artifact', 'ids', 'fingerprints', 'severity']),
|
|
274
|
-
'verify-fix': new Set(['artifact', 'review', 'fixChange', 'severity', 'minIssues']),
|
|
275
|
-
'create-change-request': new Set([
|
|
276
|
-
'platform',
|
|
277
|
-
'owner',
|
|
278
|
-
'repo',
|
|
279
|
-
'project',
|
|
280
|
-
'projectId',
|
|
281
|
-
'sourceBranch',
|
|
282
|
-
'head',
|
|
283
|
-
'targetBranch',
|
|
284
|
-
'base',
|
|
285
|
-
'title',
|
|
286
|
-
'body',
|
|
287
|
-
'draft',
|
|
288
|
-
'reuseExisting',
|
|
289
|
-
]),
|
|
290
|
-
'create-pr': new Set([
|
|
291
|
-
'platform',
|
|
292
|
-
'owner',
|
|
293
|
-
'repo',
|
|
294
|
-
'project',
|
|
295
|
-
'projectId',
|
|
296
|
-
'sourceBranch',
|
|
297
|
-
'head',
|
|
298
|
-
'targetBranch',
|
|
299
|
-
'base',
|
|
300
|
-
'title',
|
|
301
|
-
'body',
|
|
302
|
-
'draft',
|
|
303
|
-
'reuseExisting',
|
|
304
|
-
]),
|
|
305
|
-
'create-mr': new Set([
|
|
306
|
-
'platform',
|
|
307
|
-
'owner',
|
|
308
|
-
'repo',
|
|
309
|
-
'project',
|
|
310
|
-
'projectId',
|
|
311
|
-
'sourceBranch',
|
|
312
|
-
'head',
|
|
313
|
-
'targetBranch',
|
|
314
|
-
'base',
|
|
315
|
-
'title',
|
|
316
|
-
'body',
|
|
317
|
-
'draft',
|
|
318
|
-
'reuseExisting',
|
|
319
|
-
]),
|
|
320
|
-
'change-source': new Set([
|
|
321
|
-
'type',
|
|
322
|
-
'staged',
|
|
323
|
-
'from',
|
|
324
|
-
'to',
|
|
325
|
-
'includePrereleaseFrom',
|
|
326
|
-
'owner',
|
|
327
|
-
'repo',
|
|
328
|
-
'pr',
|
|
329
|
-
'project',
|
|
330
|
-
'projectId',
|
|
331
|
-
'mr',
|
|
332
|
-
'mrIid',
|
|
333
|
-
'source',
|
|
334
|
-
'fixChange',
|
|
335
|
-
]),
|
|
336
|
-
review: new Set(['source', 'reviewArtifact', 'severity', 'artifact']),
|
|
337
|
-
'review-context': new Set(['source', 'file', 'baseBranch']),
|
|
338
|
-
describe: new Set(['source', 'post', 'postDescription']),
|
|
339
|
-
'code-quality-report': new Set(['review', 'path']),
|
|
340
|
-
'post-comment': new Set([
|
|
341
|
-
'source',
|
|
342
|
-
'platform',
|
|
343
|
-
'owner',
|
|
344
|
-
'repo',
|
|
345
|
-
'project',
|
|
346
|
-
'projectId',
|
|
347
|
-
'pr',
|
|
348
|
-
'mr',
|
|
349
|
-
'prNumber',
|
|
350
|
-
'mrIid',
|
|
351
|
-
'body',
|
|
352
|
-
'marker',
|
|
353
|
-
]),
|
|
354
|
-
'post-review-comments': new Set(['source', 'review', 'removeErrorComment']),
|
|
355
|
-
'post-fix-status': new Set([
|
|
356
|
-
'platform',
|
|
357
|
-
'owner',
|
|
358
|
-
'repo',
|
|
359
|
-
'project',
|
|
360
|
-
'projectId',
|
|
361
|
-
'pr',
|
|
362
|
-
'mr',
|
|
363
|
-
'source',
|
|
364
|
-
'reviewArtifact',
|
|
365
|
-
'fixReview',
|
|
366
|
-
'fixChange',
|
|
367
|
-
'severity',
|
|
368
|
-
'stackedPrUrl',
|
|
369
|
-
'marker',
|
|
370
|
-
]),
|
|
371
|
-
};
|
|
372
|
-
function validateAllowedFields(nodeId, value, allowed, subject) {
|
|
373
|
-
const unknownFields = Object.keys(value).filter((key) => !allowed.has(key));
|
|
374
|
-
if (unknownFields.length > 0) {
|
|
375
|
-
throw new Error(`Workflow ${subject} "${nodeId}" has unsupported field(s): ${unknownFields.join(', ')}.`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
function validateWorkflowNodeShape(nodeId, node) {
|
|
379
|
-
validateAllowedFields(nodeId, node, WORKFLOW_NODE_FIELDS, 'node');
|
|
380
|
-
const kind = getNodeKind(node);
|
|
381
|
-
if (kind === 'control') {
|
|
382
|
-
const control = node.control;
|
|
383
|
-
if (!control || !CONTROL_NODE_FIELDS[control]) {
|
|
384
|
-
throw new Error(`Workflow control node "${nodeId}" has unsupported control "${String(control)}".`);
|
|
385
|
-
}
|
|
386
|
-
validateAllowedFields(nodeId, node, CONTROL_NODE_FIELDS[control], 'control node');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
validateAllowedFields(nodeId, node, EXECUTABLE_NODE_FIELDS, 'node');
|
|
390
|
-
if (node.action && node.with) {
|
|
391
|
-
const allowed = ACTION_OPTION_FIELDS[node.action];
|
|
392
|
-
if (!allowed) {
|
|
393
|
-
throw new Error(`Workflow action node "${nodeId}" has unsupported action "${node.action}".`);
|
|
394
|
-
}
|
|
395
|
-
validateAllowedFields(nodeId, node.with, allowed, `node "${nodeId}" with`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function validateWorkflowNodeKinds(nodes) {
|
|
399
|
-
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
400
|
-
validateWorkflowNodeShape(nodeId, node);
|
|
401
|
-
validateWorkflowPassThroughShape(nodeId, node);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
function hasWorkflowControlNodes(nodes) {
|
|
405
|
-
return Object.values(nodes).some((node) => node.control !== undefined);
|
|
406
|
-
}
|
|
407
|
-
function getWorkflowExecutionOrder(nodes) {
|
|
408
|
-
const nodeIds = Object.keys(nodes);
|
|
409
|
-
const visiting = new Set();
|
|
410
|
-
const visited = new Set();
|
|
411
|
-
const order = [];
|
|
412
|
-
function visit(nodeId) {
|
|
413
|
-
if (visited.has(nodeId)) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
if (visiting.has(nodeId)) {
|
|
417
|
-
throw new Error(`Workflow contains a dependency cycle at node "${nodeId}".`);
|
|
418
|
-
}
|
|
419
|
-
const node = nodes[nodeId];
|
|
420
|
-
if (!node) {
|
|
421
|
-
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
422
|
-
}
|
|
423
|
-
visiting.add(nodeId);
|
|
424
|
-
for (const dependency of getNodeNeeds(node)) {
|
|
425
|
-
if (!nodes[dependency]) {
|
|
426
|
-
throw new Error(`Workflow node "${nodeId}" needs unknown node "${dependency}".`);
|
|
427
|
-
}
|
|
428
|
-
visit(dependency);
|
|
429
|
-
}
|
|
430
|
-
visiting.delete(nodeId);
|
|
431
|
-
visited.add(nodeId);
|
|
432
|
-
order.push(nodeId);
|
|
433
|
-
}
|
|
434
|
-
for (const nodeId of nodeIds) {
|
|
435
|
-
visit(nodeId);
|
|
436
|
-
}
|
|
437
|
-
validateWorkflowNodeKinds(nodes);
|
|
438
|
-
validateWorkflowControlTargets(nodes);
|
|
439
|
-
const standaloneEndNodes = order.filter((nodeId) => nodes[nodeId]?.control === 'end' && getNodeNeeds(nodes[nodeId] ?? {}).length === 0);
|
|
440
|
-
const nonStandaloneEndNodes = order.filter((nodeId) => !standaloneEndNodes.includes(nodeId));
|
|
441
|
-
const executionOrder = [...nonStandaloneEndNodes, ...standaloneEndNodes];
|
|
442
|
-
validateWorkflowControlRouteDirection(nodes, executionOrder);
|
|
443
|
-
return executionOrder;
|
|
444
|
-
}
|
|
445
|
-
function getWorkflowNodes(workflowName, workflow) {
|
|
446
|
-
const nodes = workflow.nodes;
|
|
447
|
-
if (typeof nodes !== 'object' ||
|
|
448
|
-
nodes === null ||
|
|
449
|
-
Array.isArray(nodes) ||
|
|
450
|
-
Object.keys(nodes).length === 0) {
|
|
451
|
-
throw new Error(`Workflow "${workflowName}" must define at least one node.`);
|
|
452
|
-
}
|
|
453
|
-
return nodes;
|
|
454
|
-
}
|
|
455
|
-
function getWorkflowExecutionWaves(nodes, executionOrder) {
|
|
456
|
-
const depthByNode = new Map();
|
|
457
|
-
const waves = [];
|
|
458
|
-
for (const nodeId of executionOrder) {
|
|
459
|
-
const node = nodes[nodeId];
|
|
460
|
-
if (!node) {
|
|
461
|
-
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
462
|
-
}
|
|
463
|
-
const depth = getNodeNeeds(node).reduce((maxDepth, dependency) => {
|
|
464
|
-
return Math.max(maxDepth, (depthByNode.get(dependency) ?? 0) + 1);
|
|
465
|
-
}, 0);
|
|
466
|
-
depthByNode.set(nodeId, depth);
|
|
467
|
-
waves[depth] = waves[depth] ?? [];
|
|
468
|
-
waves[depth].push(nodeId);
|
|
469
|
-
}
|
|
470
|
-
return waves;
|
|
471
|
-
}
|
|
472
|
-
function getPathValue(root, path) {
|
|
473
|
-
return path.split('.').reduce((current, part) => {
|
|
474
|
-
if (current === undefined || current === null) {
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
if (typeof current !== 'object') {
|
|
478
|
-
return undefined;
|
|
479
|
-
}
|
|
480
|
-
return current[part];
|
|
481
|
-
}, root);
|
|
482
|
-
}
|
|
483
|
-
function stringifyTemplateValue(value) {
|
|
484
|
-
if (value === undefined) {
|
|
485
|
-
return '';
|
|
486
|
-
}
|
|
487
|
-
if (typeof value === 'string') {
|
|
488
|
-
return value;
|
|
489
|
-
}
|
|
490
|
-
return JSON.stringify(value, null, 2);
|
|
491
|
-
}
|
|
492
|
-
function renderTemplate(template, context) {
|
|
493
|
-
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, rawPath) => {
|
|
494
|
-
const path = rawPath.trim();
|
|
495
|
-
const value = getPathValue(context, path);
|
|
496
|
-
if (value === undefined) {
|
|
497
|
-
throw new Error(`Unknown workflow template value "{{${path}}}".`);
|
|
498
|
-
}
|
|
499
|
-
return stringifyTemplateValue(value);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
87
|
async function flushWorkflowTrace(traceCollector, workflowName, inputs, startedAt, workingDir, options) {
|
|
503
88
|
const workflowTrace = traceCollector.buildWorkflowTrace(workflowName, inputs, startedAt);
|
|
504
89
|
const scope = {
|
|
@@ -540,9 +125,6 @@ async function resolveWorkflowInput(key, input, workingDir) {
|
|
|
540
125
|
}
|
|
541
126
|
return '';
|
|
542
127
|
}
|
|
543
|
-
function getWorkflowInputConfigType(input) {
|
|
544
|
-
return typeof input === 'string' ? 'string' : (input.type ?? 'string');
|
|
545
|
-
}
|
|
546
128
|
function validateResolvedWorkflowInput(key, input, value) {
|
|
547
129
|
if (typeof input === 'string') {
|
|
548
130
|
return;
|
|
@@ -600,19 +182,6 @@ function resolveAgentsFrom(config, agentsFrom) {
|
|
|
600
182
|
}
|
|
601
183
|
throw new Error(`Unsupported workflow agentsFrom "${agentsFrom}". ` + 'Currently supported: review.agents.');
|
|
602
184
|
}
|
|
603
|
-
function getNodeKind(node) {
|
|
604
|
-
const configuredKinds = [node.agent, node.agentsFrom, node.action, node.control].filter((value) => value !== undefined).length;
|
|
605
|
-
if (configuredKinds !== 1) {
|
|
606
|
-
throw new Error('Workflow node must define exactly one of agent, agentsFrom, action, or control.');
|
|
607
|
-
}
|
|
608
|
-
if (node.agent !== undefined)
|
|
609
|
-
return 'agent';
|
|
610
|
-
if (node.agentsFrom !== undefined)
|
|
611
|
-
return 'agents';
|
|
612
|
-
if (node.control !== undefined)
|
|
613
|
-
return 'control';
|
|
614
|
-
return 'action';
|
|
615
|
-
}
|
|
616
185
|
function hasConfiguredAgentPrompt(config, agentId) {
|
|
617
186
|
const runConfig = resolveAgentRunConfig(config, agentId);
|
|
618
187
|
return runConfig.prompt !== undefined || runConfig.promptFile !== undefined;
|
|
@@ -2041,7 +1610,10 @@ async function runPostCommentWorkflowNode(nodeId, node, options, workingDir, con
|
|
|
2041
1610
|
const rawBody = node.input === undefined
|
|
2042
1611
|
? requireStringActionOption(nodeId, node, 'body', context)
|
|
2043
1612
|
: renderTemplate(node.input, context);
|
|
2044
|
-
const
|
|
1613
|
+
const configuredMarker = getStringActionOption(node, 'marker', context)?.trim();
|
|
1614
|
+
const marker = configuredMarker && configuredMarker.length > 0
|
|
1615
|
+
? configuredMarker
|
|
1616
|
+
: options.idempotencyContext?.idempotencyKey;
|
|
2045
1617
|
const body = formatMarkedComment(rawBody, marker);
|
|
2046
1618
|
let operation = 'created';
|
|
2047
1619
|
if (marker) {
|
|
@@ -2450,19 +2022,25 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
2450
2022
|
if (writes) {
|
|
2451
2023
|
await writeWorkflowFile(workingDir, writes, JSON.stringify(reviewResult, null, 2));
|
|
2452
2024
|
}
|
|
2453
|
-
const
|
|
2025
|
+
const explicitArtifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
|
|
2026
|
+
let artifactOutput;
|
|
2027
|
+
if (explicitArtifactOutput) {
|
|
2028
|
+
artifactOutput = explicitArtifactOutput;
|
|
2029
|
+
}
|
|
2030
|
+
else if (!reviewArtifactName) {
|
|
2031
|
+
artifactOutput = `${nodeId}Artifact`;
|
|
2032
|
+
}
|
|
2454
2033
|
const outputs = {};
|
|
2455
|
-
|
|
2034
|
+
const reviewArtifactPayload = createReviewArtifactPayload(reviewResult, source);
|
|
2035
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
2036
|
+
const saved = await saveWorkflowArtifact(workingDir, {
|
|
2037
|
+
kind: 'review',
|
|
2038
|
+
scope,
|
|
2039
|
+
payload: reviewArtifactPayload,
|
|
2040
|
+
});
|
|
2041
|
+
const artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
|
|
2456
2042
|
if (artifactOutput) {
|
|
2457
|
-
const reviewArtifact = createReviewArtifactPayload(reviewResult, source);
|
|
2458
|
-
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
2459
|
-
const saved = await saveWorkflowArtifact(workingDir, {
|
|
2460
|
-
kind: 'review',
|
|
2461
|
-
scope,
|
|
2462
|
-
payload: reviewArtifact,
|
|
2463
|
-
});
|
|
2464
2043
|
outputs[artifactOutput] = { ...saved.artifact, path: saved.path, latestPath: saved.latestPath };
|
|
2465
|
-
artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
|
|
2466
2044
|
}
|
|
2467
2045
|
return {
|
|
2468
2046
|
id: nodeId,
|
|
@@ -2486,272 +2064,6 @@ function recordNodeArtifact(nodeId, node, result, artifacts) {
|
|
|
2486
2064
|
}
|
|
2487
2065
|
}
|
|
2488
2066
|
}
|
|
2489
|
-
function parseWorkflowExpressionValue(value, context) {
|
|
2490
|
-
const trimmed = value.trim();
|
|
2491
|
-
if (!trimmed)
|
|
2492
|
-
return '';
|
|
2493
|
-
const templateReference = trimmed.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
2494
|
-
if (templateReference && context) {
|
|
2495
|
-
const path = templateReference[1]?.trim() ?? '';
|
|
2496
|
-
const resolved = getPathValue(context, path);
|
|
2497
|
-
if (resolved === undefined) {
|
|
2498
|
-
throw new Error(`Unknown workflow template value "{{${path}}}".`);
|
|
2499
|
-
}
|
|
2500
|
-
return resolved;
|
|
2501
|
-
}
|
|
2502
|
-
if (trimmed === 'true')
|
|
2503
|
-
return true;
|
|
2504
|
-
if (trimmed === 'false')
|
|
2505
|
-
return false;
|
|
2506
|
-
if (trimmed === 'null')
|
|
2507
|
-
return null;
|
|
2508
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
2509
|
-
return Number(trimmed);
|
|
2510
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
2511
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
2512
|
-
const inner = trimmed.slice(1, -1);
|
|
2513
|
-
return context ? renderTemplate(inner, context) : inner;
|
|
2514
|
-
}
|
|
2515
|
-
if (context && /^(inputs|nodes|artifacts|loop)\.[A-Za-z0-9_.-]+$/.test(trimmed)) {
|
|
2516
|
-
const resolved = getPathValue(context, trimmed);
|
|
2517
|
-
if (resolved === undefined) {
|
|
2518
|
-
throw new Error(`Unknown workflow expression value "${trimmed}".`);
|
|
2519
|
-
}
|
|
2520
|
-
return resolved;
|
|
2521
|
-
}
|
|
2522
|
-
try {
|
|
2523
|
-
return JSON.parse(trimmed);
|
|
2524
|
-
}
|
|
2525
|
-
catch {
|
|
2526
|
-
return trimmed;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
function isWorkflowTruthy(value) {
|
|
2530
|
-
if (value === undefined || value === null)
|
|
2531
|
-
return false;
|
|
2532
|
-
if (typeof value === 'boolean')
|
|
2533
|
-
return value;
|
|
2534
|
-
if (typeof value === 'number')
|
|
2535
|
-
return value !== 0;
|
|
2536
|
-
if (typeof value === 'string') {
|
|
2537
|
-
const normalized = value.trim().toLowerCase();
|
|
2538
|
-
return (normalized.length > 0 && normalized !== 'false' && normalized !== '0' && normalized !== 'no');
|
|
2539
|
-
}
|
|
2540
|
-
if (Array.isArray(value))
|
|
2541
|
-
return value.length > 0;
|
|
2542
|
-
if (typeof value === 'object')
|
|
2543
|
-
return Object.keys(value).length > 0;
|
|
2544
|
-
return true;
|
|
2545
|
-
}
|
|
2546
|
-
function normalizeWorkflowBooleanLike(value) {
|
|
2547
|
-
if (typeof value === 'boolean')
|
|
2548
|
-
return value;
|
|
2549
|
-
if (typeof value === 'number') {
|
|
2550
|
-
if (value === 1)
|
|
2551
|
-
return true;
|
|
2552
|
-
if (value === 0)
|
|
2553
|
-
return false;
|
|
2554
|
-
}
|
|
2555
|
-
if (typeof value === 'string') {
|
|
2556
|
-
const normalized = value.trim().toLowerCase();
|
|
2557
|
-
if (normalized === 'true' || normalized === '1' || normalized === 'yes')
|
|
2558
|
-
return true;
|
|
2559
|
-
if (normalized === 'false' || normalized === '0' || normalized === 'no')
|
|
2560
|
-
return false;
|
|
2561
|
-
}
|
|
2562
|
-
return undefined;
|
|
2563
|
-
}
|
|
2564
|
-
function compareWorkflowValues(left, operator, right) {
|
|
2565
|
-
if (operator === '==' || operator === '!=') {
|
|
2566
|
-
const leftBoolean = normalizeWorkflowBooleanLike(left);
|
|
2567
|
-
const rightBoolean = normalizeWorkflowBooleanLike(right);
|
|
2568
|
-
const matches = leftBoolean !== undefined || rightBoolean !== undefined
|
|
2569
|
-
? leftBoolean === rightBoolean
|
|
2570
|
-
: String(left) === String(right);
|
|
2571
|
-
return operator === '==' ? matches : !matches;
|
|
2572
|
-
}
|
|
2573
|
-
const leftNumber = Number(left);
|
|
2574
|
-
const rightNumber = Number(right);
|
|
2575
|
-
if (!Number.isFinite(leftNumber) || !Number.isFinite(rightNumber)) {
|
|
2576
|
-
throw new Error(`Workflow expression operator "${operator}" requires numeric values.`);
|
|
2577
|
-
}
|
|
2578
|
-
if (operator === '>')
|
|
2579
|
-
return leftNumber > rightNumber;
|
|
2580
|
-
if (operator === '>=')
|
|
2581
|
-
return leftNumber >= rightNumber;
|
|
2582
|
-
if (operator === '<')
|
|
2583
|
-
return leftNumber < rightNumber;
|
|
2584
|
-
if (operator === '<=')
|
|
2585
|
-
return leftNumber <= rightNumber;
|
|
2586
|
-
throw new Error(`Unsupported workflow expression operator "${operator}".`);
|
|
2587
|
-
}
|
|
2588
|
-
function splitWorkflowExpressionOperator(expression, operator) {
|
|
2589
|
-
const parts = [];
|
|
2590
|
-
let current = '';
|
|
2591
|
-
let quote;
|
|
2592
|
-
let depth = 0;
|
|
2593
|
-
for (let i = 0; i < expression.length; i++) {
|
|
2594
|
-
const char = expression[i] ?? '';
|
|
2595
|
-
if ((char === '"' || char === "'") && expression[i - 1] !== '\\') {
|
|
2596
|
-
quote = quote === char ? undefined : (quote ?? char);
|
|
2597
|
-
current += char;
|
|
2598
|
-
continue;
|
|
2599
|
-
}
|
|
2600
|
-
if (!quote && char === '(') {
|
|
2601
|
-
depth += 1;
|
|
2602
|
-
current += char;
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
if (!quote && char === ')' && depth > 0) {
|
|
2606
|
-
depth -= 1;
|
|
2607
|
-
current += char;
|
|
2608
|
-
continue;
|
|
2609
|
-
}
|
|
2610
|
-
if (!quote && depth === 0 && expression.slice(i, i + operator.length) === operator) {
|
|
2611
|
-
parts.push(current.trim());
|
|
2612
|
-
current = '';
|
|
2613
|
-
i += operator.length - 1;
|
|
2614
|
-
continue;
|
|
2615
|
-
}
|
|
2616
|
-
current += char;
|
|
2617
|
-
}
|
|
2618
|
-
parts.push(current.trim());
|
|
2619
|
-
return parts;
|
|
2620
|
-
}
|
|
2621
|
-
function stripWorkflowExpressionParens(expression) {
|
|
2622
|
-
const trimmed = expression.trim();
|
|
2623
|
-
if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
|
|
2624
|
-
return trimmed;
|
|
2625
|
-
}
|
|
2626
|
-
let quote;
|
|
2627
|
-
let depth = 0;
|
|
2628
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
2629
|
-
const char = trimmed[i] ?? '';
|
|
2630
|
-
if ((char === '"' || char === "'") && trimmed[i - 1] !== '\\') {
|
|
2631
|
-
quote = quote === char ? undefined : (quote ?? char);
|
|
2632
|
-
continue;
|
|
2633
|
-
}
|
|
2634
|
-
if (quote)
|
|
2635
|
-
continue;
|
|
2636
|
-
if (char === '(')
|
|
2637
|
-
depth += 1;
|
|
2638
|
-
if (char === ')')
|
|
2639
|
-
depth -= 1;
|
|
2640
|
-
if (depth === 0 && i < trimmed.length - 1) {
|
|
2641
|
-
return trimmed;
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
return trimmed.slice(1, -1).trim();
|
|
2645
|
-
}
|
|
2646
|
-
function evaluateWorkflowExpressionText(rendered, context) {
|
|
2647
|
-
rendered = stripWorkflowExpressionParens(rendered);
|
|
2648
|
-
const orParts = splitWorkflowExpressionOperator(rendered, '||');
|
|
2649
|
-
if (orParts.length > 1) {
|
|
2650
|
-
return orParts.some((part) => evaluateWorkflowExpressionText(part, context));
|
|
2651
|
-
}
|
|
2652
|
-
const andParts = splitWorkflowExpressionOperator(rendered, '&&');
|
|
2653
|
-
if (andParts.length > 1) {
|
|
2654
|
-
return andParts.every((part) => evaluateWorkflowExpressionText(part, context));
|
|
2655
|
-
}
|
|
2656
|
-
const match = rendered.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
2657
|
-
if (!match) {
|
|
2658
|
-
return isWorkflowTruthy(parseWorkflowExpressionValue(rendered, context));
|
|
2659
|
-
}
|
|
2660
|
-
return compareWorkflowValues(parseWorkflowExpressionValue(match[1] ?? '', context), match[2] ?? '', parseWorkflowExpressionValue(match[3] ?? '', context));
|
|
2661
|
-
}
|
|
2662
|
-
function evaluateWorkflowExpression(expression, context) {
|
|
2663
|
-
return evaluateWorkflowExpressionText(expression.trim(), context);
|
|
2664
|
-
}
|
|
2665
|
-
function splitWorkflowSegments(workflowNodes, executionOrder) {
|
|
2666
|
-
const segments = [];
|
|
2667
|
-
let currentDag = [];
|
|
2668
|
-
for (const nodeId of executionOrder) {
|
|
2669
|
-
const node = workflowNodes[nodeId];
|
|
2670
|
-
if (!node) {
|
|
2671
|
-
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
2672
|
-
}
|
|
2673
|
-
if (node.control !== undefined) {
|
|
2674
|
-
if (currentDag.length > 0) {
|
|
2675
|
-
segments.push({ type: 'dag', nodeIds: currentDag });
|
|
2676
|
-
currentDag = [];
|
|
2677
|
-
}
|
|
2678
|
-
segments.push({ type: 'control', nodeId });
|
|
2679
|
-
}
|
|
2680
|
-
else {
|
|
2681
|
-
currentDag.push(nodeId);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
if (currentDag.length > 0) {
|
|
2685
|
-
segments.push({ type: 'dag', nodeIds: currentDag });
|
|
2686
|
-
}
|
|
2687
|
-
return segments;
|
|
2688
|
-
}
|
|
2689
|
-
function findWorkflowSegmentIndex(segments, targetNodeId) {
|
|
2690
|
-
return segments.findIndex((segment) => segment.type === 'control'
|
|
2691
|
-
? segment.nodeId === targetNodeId
|
|
2692
|
-
: segment.nodeIds.includes(targetNodeId));
|
|
2693
|
-
}
|
|
2694
|
-
function computeActiveWorkflowNodes(workflowNodes, nodeIds, rootNodeId, includeRootDependencies = true) {
|
|
2695
|
-
const segmentNodeIds = new Set(nodeIds);
|
|
2696
|
-
const downstream = new Set([rootNodeId]);
|
|
2697
|
-
let changed = true;
|
|
2698
|
-
while (changed) {
|
|
2699
|
-
changed = false;
|
|
2700
|
-
for (const nodeId of nodeIds) {
|
|
2701
|
-
if (downstream.has(nodeId))
|
|
2702
|
-
continue;
|
|
2703
|
-
const needs = getNodeNeeds(workflowNodes[nodeId] ?? {});
|
|
2704
|
-
if (needs.some((dependency) => downstream.has(dependency))) {
|
|
2705
|
-
downstream.add(nodeId);
|
|
2706
|
-
changed = true;
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
const active = new Set(downstream);
|
|
2711
|
-
const includeDependencies = (nodeId) => {
|
|
2712
|
-
const node = workflowNodes[nodeId];
|
|
2713
|
-
if (!node)
|
|
2714
|
-
return;
|
|
2715
|
-
for (const dependency of getNodeNeeds(node)) {
|
|
2716
|
-
if (!segmentNodeIds.has(dependency) || active.has(dependency))
|
|
2717
|
-
continue;
|
|
2718
|
-
active.add(dependency);
|
|
2719
|
-
includeDependencies(dependency);
|
|
2720
|
-
}
|
|
2721
|
-
};
|
|
2722
|
-
if (includeRootDependencies) {
|
|
2723
|
-
for (const nodeId of downstream) {
|
|
2724
|
-
includeDependencies(nodeId);
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
return active;
|
|
2728
|
-
}
|
|
2729
|
-
function createSkippedWorkflowNodeResult(nodeId) {
|
|
2730
|
-
return {
|
|
2731
|
-
id: nodeId,
|
|
2732
|
-
type: 'skipped',
|
|
2733
|
-
status: 'skipped',
|
|
2734
|
-
response: '',
|
|
2735
|
-
output: undefined,
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
function getWorkflowNodeRunCondition(node) {
|
|
2739
|
-
return node.if;
|
|
2740
|
-
}
|
|
2741
|
-
function getSkippedWorkflowDependencies(node, nodes) {
|
|
2742
|
-
return getNodeNeeds(node).filter((dependency) => nodes[dependency]?.status === 'skipped');
|
|
2743
|
-
}
|
|
2744
|
-
function getWorkflowNodeSkipReason(node, context) {
|
|
2745
|
-
const skippedDependencies = getSkippedWorkflowDependencies(node, context.nodes);
|
|
2746
|
-
if (skippedDependencies.length > 0) {
|
|
2747
|
-
return `dependency skipped: ${skippedDependencies.join(', ')}`;
|
|
2748
|
-
}
|
|
2749
|
-
const ifExpression = getWorkflowNodeRunCondition(node);
|
|
2750
|
-
if (ifExpression !== undefined && !evaluateWorkflowExpression(ifExpression, context)) {
|
|
2751
|
-
return `if false: ${ifExpression}`;
|
|
2752
|
-
}
|
|
2753
|
-
return undefined;
|
|
2754
|
-
}
|
|
2755
2067
|
function logWorkflowNodeRunning(nodeId, options) {
|
|
2756
2068
|
if (!options.jsonOutput) {
|
|
2757
2069
|
console.log(chalk.gray(`Running node ${nodeId}...`));
|
|
@@ -2769,7 +2081,14 @@ function recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts) {
|
|
|
2769
2081
|
recordNodeArtifact(nodeId, node, result, artifacts);
|
|
2770
2082
|
}
|
|
2771
2083
|
}
|
|
2772
|
-
|
|
2084
|
+
function completeWorkflowNodeResult(result, startedAt) {
|
|
2085
|
+
const completedAt = new Date().toISOString();
|
|
2086
|
+
result.startedAt ??= startedAt;
|
|
2087
|
+
result.completedAt ??= completedAt;
|
|
2088
|
+
result.durationMs ??= Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime());
|
|
2089
|
+
return result;
|
|
2090
|
+
}
|
|
2091
|
+
async function runWorkflowDagSegment(workflowNodes, nodeIds, activeNodeIds, options, context, nodeExecutor) {
|
|
2773
2092
|
const completed = new Set();
|
|
2774
2093
|
const segmentNodeIds = new Set(nodeIds);
|
|
2775
2094
|
if (activeNodeIds) {
|
|
@@ -2800,14 +2119,19 @@ async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeI
|
|
|
2800
2119
|
if (!node) {
|
|
2801
2120
|
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
2802
2121
|
}
|
|
2122
|
+
const startedAt = new Date().toISOString();
|
|
2803
2123
|
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
2804
2124
|
if (skipReason) {
|
|
2805
2125
|
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
2806
|
-
return {
|
|
2126
|
+
return {
|
|
2127
|
+
nodeId,
|
|
2128
|
+
node,
|
|
2129
|
+
result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
|
|
2130
|
+
};
|
|
2807
2131
|
}
|
|
2808
2132
|
logWorkflowNodeRunning(nodeId, options);
|
|
2809
|
-
const result = await
|
|
2810
|
-
return { nodeId, node, result };
|
|
2133
|
+
const result = await nodeExecutor.runNode(nodeId, node, context);
|
|
2134
|
+
return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
|
|
2811
2135
|
}));
|
|
2812
2136
|
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
2813
2137
|
if (firstRejection) {
|
|
@@ -2822,124 +2146,22 @@ async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeI
|
|
|
2822
2146
|
}
|
|
2823
2147
|
}
|
|
2824
2148
|
}
|
|
2825
|
-
function
|
|
2826
|
-
|
|
2827
|
-
const expression = node.if;
|
|
2828
|
-
if (!expression) {
|
|
2829
|
-
throw new Error(`Workflow loop node "${nodeId}" must define if.`);
|
|
2830
|
-
}
|
|
2831
|
-
if (!node.target || !node.exit) {
|
|
2832
|
-
throw new Error(`Workflow loop node "${nodeId}" must define target and exit.`);
|
|
2833
|
-
}
|
|
2834
|
-
const rawMaxIterations = node.maxIterations;
|
|
2835
|
-
const renderedMaxIterations = typeof rawMaxIterations === 'string'
|
|
2836
|
-
? renderTemplate(rawMaxIterations, context).trim()
|
|
2837
|
-
: String(rawMaxIterations ?? '');
|
|
2838
|
-
const configuredMaxIterations = Number.parseInt(renderedMaxIterations, 10);
|
|
2839
|
-
if (renderedMaxIterations === '' ||
|
|
2840
|
-
!Number.isInteger(configuredMaxIterations) ||
|
|
2841
|
-
configuredMaxIterations <= 0) {
|
|
2842
|
-
throw new Error(`Workflow loop node "${nodeId}" must define a positive maxIterations.`);
|
|
2843
|
-
}
|
|
2844
|
-
const maxIterations = configuredMaxIterations;
|
|
2845
|
-
const shouldLoop = evaluateWorkflowExpression(expression, context);
|
|
2846
|
-
const current = context.loop[nodeId] ?? { iteration: 1, maxIterations };
|
|
2847
|
-
let nextNodeId = node.exit;
|
|
2848
|
-
let decision = 'exit';
|
|
2849
|
-
if (shouldLoop) {
|
|
2850
|
-
if (current.iteration >= maxIterations) {
|
|
2851
|
-
if (node.onMaxIterations === 'exit') {
|
|
2852
|
-
nextNodeId = node.exit;
|
|
2853
|
-
}
|
|
2854
|
-
else {
|
|
2855
|
-
throw new Error(`Workflow loop node "${nodeId}" reached maxIterations (${maxIterations}).`);
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
else {
|
|
2859
|
-
decision = 'loop';
|
|
2860
|
-
nextNodeId = node.target;
|
|
2861
|
-
current.iteration += 1;
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
current.maxIterations = maxIterations;
|
|
2865
|
-
current.lastDecision = decision;
|
|
2866
|
-
context.loop[nodeId] = current;
|
|
2867
|
-
return {
|
|
2868
|
-
nextNodeId,
|
|
2869
|
-
result: {
|
|
2870
|
-
id: nodeId,
|
|
2871
|
-
type: 'control',
|
|
2872
|
-
status: 'success',
|
|
2873
|
-
control: node.control,
|
|
2874
|
-
decision,
|
|
2875
|
-
target: nextNodeId,
|
|
2876
|
-
response: decision,
|
|
2877
|
-
output: {
|
|
2878
|
-
matched: shouldLoop,
|
|
2879
|
-
target: nextNodeId,
|
|
2880
|
-
iteration: current.iteration,
|
|
2881
|
-
maxIterations,
|
|
2882
|
-
},
|
|
2883
|
-
},
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
if (node.control === 'switch') {
|
|
2887
|
-
if (!node.value || !node.cases) {
|
|
2888
|
-
throw new Error(`Workflow switch node "${nodeId}" must define value and cases.`);
|
|
2889
|
-
}
|
|
2890
|
-
const value = renderTemplate(node.value, context).trim();
|
|
2891
|
-
const nextNodeId = node.cases[value] ?? node.default;
|
|
2892
|
-
if (!nextNodeId) {
|
|
2893
|
-
throw new Error(`Workflow switch node "${nodeId}" has no case for "${value}" or default.`);
|
|
2894
|
-
}
|
|
2895
|
-
return {
|
|
2896
|
-
nextNodeId,
|
|
2897
|
-
result: {
|
|
2898
|
-
id: nodeId,
|
|
2899
|
-
type: 'control',
|
|
2900
|
-
status: 'success',
|
|
2901
|
-
control: node.control,
|
|
2902
|
-
decision: value,
|
|
2903
|
-
target: nextNodeId,
|
|
2904
|
-
response: value,
|
|
2905
|
-
output: { value, target: nextNodeId },
|
|
2906
|
-
},
|
|
2907
|
-
};
|
|
2908
|
-
}
|
|
2909
|
-
if (node.control === 'end') {
|
|
2910
|
-
return {
|
|
2911
|
-
ended: true,
|
|
2912
|
-
result: {
|
|
2913
|
-
id: nodeId,
|
|
2914
|
-
type: 'control',
|
|
2915
|
-
status: 'success',
|
|
2916
|
-
control: node.control,
|
|
2917
|
-
decision: 'end',
|
|
2918
|
-
response: 'end',
|
|
2919
|
-
output: { ended: true },
|
|
2920
|
-
},
|
|
2921
|
-
};
|
|
2922
|
-
}
|
|
2923
|
-
if (node.control === 'passThrough') {
|
|
2924
|
-
const result = {
|
|
2925
|
-
id: nodeId,
|
|
2926
|
-
type: 'control',
|
|
2927
|
-
control: 'passThrough',
|
|
2928
|
-
target: node.target,
|
|
2929
|
-
decision: 'pass',
|
|
2930
|
-
response: `passed through to ${node.target}`,
|
|
2931
|
-
};
|
|
2932
|
-
return { result, nextNodeId: node.target };
|
|
2933
|
-
}
|
|
2934
|
-
throw new Error(`Unsupported workflow control "${node.control}" in node "${nodeId}".`);
|
|
2149
|
+
async function runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor) {
|
|
2150
|
+
await runControlWorkflowFromSegments(workflowNodes, splitWorkflowSegments(workflowNodes, executionOrder), options, context, nodeExecutor);
|
|
2935
2151
|
}
|
|
2936
|
-
async function
|
|
2937
|
-
const segments =
|
|
2152
|
+
async function runControlWorkflowFromSegments(workflowNodes, compiledSegments, options, context, nodeExecutor) {
|
|
2153
|
+
const segments = compiledSegments.map((segment) => segment.type === 'control'
|
|
2154
|
+
? { type: 'control', nodeId: segment.nodeId }
|
|
2155
|
+
: {
|
|
2156
|
+
type: 'dag',
|
|
2157
|
+
nodeIds: [...segment.nodeIds],
|
|
2158
|
+
...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
|
|
2159
|
+
});
|
|
2938
2160
|
let segmentIndex = 0;
|
|
2939
2161
|
while (segmentIndex < segments.length) {
|
|
2940
2162
|
const segment = segments[segmentIndex];
|
|
2941
2163
|
if (segment.type === 'dag') {
|
|
2942
|
-
await runWorkflowDagSegment(
|
|
2164
|
+
await runWorkflowDagSegment(workflowNodes, segment.nodeIds, segment.activeNodeIds, options, context, nodeExecutor);
|
|
2943
2165
|
segmentIndex += 1;
|
|
2944
2166
|
continue;
|
|
2945
2167
|
}
|
|
@@ -2954,9 +2176,14 @@ async function runControlWorkflow(config, workflowNodes, executionOrder, options
|
|
|
2954
2176
|
else {
|
|
2955
2177
|
logWorkflowNodeRunning(segment.nodeId, options);
|
|
2956
2178
|
}
|
|
2957
|
-
const
|
|
2179
|
+
const startedAt = new Date().toISOString();
|
|
2180
|
+
const controlResult = skipReason
|
|
2958
2181
|
? { result: createSkippedWorkflowNodeResult(segment.nodeId) }
|
|
2959
2182
|
: runControlWorkflowNode(segment.nodeId, node, context);
|
|
2183
|
+
const { result, nextNodeId, ended } = {
|
|
2184
|
+
...controlResult,
|
|
2185
|
+
result: completeWorkflowNodeResult(controlResult.result, startedAt),
|
|
2186
|
+
};
|
|
2960
2187
|
recordWorkflowNodeResult(segment.nodeId, node, result, context.nodes, context.artifacts);
|
|
2961
2188
|
if (ended) {
|
|
2962
2189
|
return;
|
|
@@ -2998,12 +2225,74 @@ async function runSingleWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
2998
2225
|
}
|
|
2999
2226
|
return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
3000
2227
|
}
|
|
2228
|
+
/**
|
|
2229
|
+
* In-process {@link NodeExecutor} that dispatches a single non-control node
|
|
2230
|
+
* to the existing side-effecting runner functions. Constructed once per
|
|
2231
|
+
* workflow run so git/platform clients and locks are shared across nodes.
|
|
2232
|
+
*
|
|
2233
|
+
* The Temporal executor will provide its own NodeExecutor whose `runNode`
|
|
2234
|
+
* schedules a Temporal activity instead of executing in-process.
|
|
2235
|
+
*/
|
|
2236
|
+
class LocalNodeExecutor {
|
|
2237
|
+
config;
|
|
2238
|
+
options;
|
|
2239
|
+
workingDir;
|
|
2240
|
+
executionContext;
|
|
2241
|
+
constructor(config, options, workingDir, executionContext) {
|
|
2242
|
+
this.config = config;
|
|
2243
|
+
this.options = options;
|
|
2244
|
+
this.workingDir = workingDir;
|
|
2245
|
+
this.executionContext = executionContext;
|
|
2246
|
+
}
|
|
2247
|
+
async runNode(nodeId, node, context) {
|
|
2248
|
+
return runSingleWorkflowNode(this.config, nodeId, node, this.options, this.workingDir, context, this.executionContext);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
export async function runWorkflowNodeLocally(config, nodeId, node, options, workingDir, context) {
|
|
2252
|
+
const executionContext = {
|
|
2253
|
+
gitClients: new Map(),
|
|
2254
|
+
platformClients: {},
|
|
2255
|
+
traceCollector: options.trace ? new TraceCollector() : undefined,
|
|
2256
|
+
locks: {
|
|
2257
|
+
exit: createWorkflowLock(),
|
|
2258
|
+
console: createWorkflowLock(),
|
|
2259
|
+
},
|
|
2260
|
+
};
|
|
2261
|
+
return runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
2262
|
+
}
|
|
3001
2263
|
export async function runWorkflow(config, workflowName, options = {}) {
|
|
3002
2264
|
const workflow = config.workflows?.[workflowName];
|
|
3003
2265
|
if (!workflow) {
|
|
3004
2266
|
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
3005
2267
|
}
|
|
3006
2268
|
const workflowNodes = getWorkflowNodes(workflowName, workflow);
|
|
2269
|
+
const executionOrder = getWorkflowExecutionOrder(workflowNodes);
|
|
2270
|
+
const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
|
|
2271
|
+
return executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflow.output, hasWorkflowControlNodes(workflowNodes), options);
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Run a workflow from a previously compiled {@link CompiledWorkflowPlan}.
|
|
2275
|
+
*
|
|
2276
|
+
* The plan supplies the scheduling data (nodes, execution order, waves,
|
|
2277
|
+
* segments, output key) so the caller does not need to reload repo config to
|
|
2278
|
+
* drive execution. `config` is still required because agent/action node
|
|
2279
|
+
* execution needs agent and review configuration that lives outside the
|
|
2280
|
+
* compiled plan.
|
|
2281
|
+
*/
|
|
2282
|
+
export async function runWorkflowFromCompiledPlan(config, plan, options = {}) {
|
|
2283
|
+
const executionSegments = plan.hasControlNodes ? plan.segments : [];
|
|
2284
|
+
if (plan.hasControlNodes) {
|
|
2285
|
+
const workflowNodes = plan.nodes;
|
|
2286
|
+
const order = plan.executionOrder;
|
|
2287
|
+
return executeWorkflowRun(config, plan.workflowName, workflowNodes, order, plan.waves, plan.output, true, options, executionSegments);
|
|
2288
|
+
}
|
|
2289
|
+
return executeWorkflowRun(config, plan.workflowName, plan.nodes, plan.executionOrder, plan.waves, plan.output, false, options);
|
|
2290
|
+
}
|
|
2291
|
+
async function executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflowOutput, hasControl, options, controlSegments) {
|
|
2292
|
+
const workflow = config.workflows?.[workflowName];
|
|
2293
|
+
if (!workflow) {
|
|
2294
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
2295
|
+
}
|
|
3007
2296
|
const workingDir = options.workingDir ?? process.cwd();
|
|
3008
2297
|
const inputs = await resolveWorkflowInputs(workflow, options, workingDir);
|
|
3009
2298
|
const nodes = {};
|
|
@@ -3019,14 +2308,25 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3019
2308
|
console: createWorkflowLock(),
|
|
3020
2309
|
},
|
|
3021
2310
|
};
|
|
3022
|
-
const
|
|
3023
|
-
const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
|
|
2311
|
+
const nodeExecutor = new LocalNodeExecutor(config, options, workingDir, executionContext);
|
|
3024
2312
|
if (!options.jsonOutput) {
|
|
3025
2313
|
console.log(chalk.gray(`Running workflow ${workflowName}...\n`));
|
|
3026
2314
|
}
|
|
3027
2315
|
try {
|
|
3028
|
-
if (
|
|
3029
|
-
|
|
2316
|
+
if (hasControl) {
|
|
2317
|
+
if (controlSegments) {
|
|
2318
|
+
const internalSegments = controlSegments.map((segment) => segment.type === 'control'
|
|
2319
|
+
? { type: 'control', nodeId: segment.nodeId }
|
|
2320
|
+
: {
|
|
2321
|
+
type: 'dag',
|
|
2322
|
+
nodeIds: segment.nodeIds,
|
|
2323
|
+
...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
|
|
2324
|
+
});
|
|
2325
|
+
await runControlWorkflowFromSegments(workflowNodes, internalSegments, options, context, nodeExecutor);
|
|
2326
|
+
}
|
|
2327
|
+
else {
|
|
2328
|
+
await runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor);
|
|
2329
|
+
}
|
|
3030
2330
|
}
|
|
3031
2331
|
else {
|
|
3032
2332
|
for (const wave of executionWaves) {
|
|
@@ -3035,14 +2335,19 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3035
2335
|
if (!node) {
|
|
3036
2336
|
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
3037
2337
|
}
|
|
2338
|
+
const startedAt = new Date().toISOString();
|
|
3038
2339
|
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
3039
2340
|
if (skipReason) {
|
|
3040
2341
|
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
3041
|
-
return {
|
|
2342
|
+
return {
|
|
2343
|
+
nodeId,
|
|
2344
|
+
node,
|
|
2345
|
+
result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
|
|
2346
|
+
};
|
|
3042
2347
|
}
|
|
3043
2348
|
logWorkflowNodeRunning(nodeId, options);
|
|
3044
|
-
const result = await
|
|
3045
|
-
return { nodeId, node, result };
|
|
2349
|
+
const result = await nodeExecutor.runNode(nodeId, node, context);
|
|
2350
|
+
return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
|
|
3046
2351
|
}));
|
|
3047
2352
|
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
3048
2353
|
if (firstRejection) {
|
|
@@ -3070,7 +2375,7 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3070
2375
|
}
|
|
3071
2376
|
const lastNodeId = executionOrder[executionOrder.length - 1];
|
|
3072
2377
|
const lastNode = workflowNodes[lastNodeId];
|
|
3073
|
-
const outputKey =
|
|
2378
|
+
const outputKey = workflowOutput ?? lastNode.output ?? lastNodeId;
|
|
3074
2379
|
const result = {
|
|
3075
2380
|
timestamp: new Date().toISOString(),
|
|
3076
2381
|
workflow: workflowName,
|
|
@@ -3119,6 +2424,7 @@ export function listWorkflows(config, options = {}) {
|
|
|
3119
2424
|
source: info.source,
|
|
3120
2425
|
overridden: info.overridesPackaged,
|
|
3121
2426
|
description: workflow.description,
|
|
2427
|
+
metadata: workflow.metadata,
|
|
3122
2428
|
};
|
|
3123
2429
|
})
|
|
3124
2430
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3171,11 +2477,13 @@ function buildWorkflowDetail(name, workflow, workingDir) {
|
|
|
3171
2477
|
};
|
|
3172
2478
|
const workflowNodes = getWorkflowNodes(name, workflow);
|
|
3173
2479
|
getWorkflowExecutionOrder(workflowNodes);
|
|
2480
|
+
const graph = buildWorkflowGraph(name, workflow);
|
|
3174
2481
|
return {
|
|
3175
2482
|
name,
|
|
3176
2483
|
source: info.source,
|
|
3177
2484
|
overridden: info.overridesPackaged,
|
|
3178
2485
|
description: workflow.description,
|
|
2486
|
+
metadata: workflow.metadata,
|
|
3179
2487
|
inputs: workflow.inputs ?? {},
|
|
3180
2488
|
output: workflow.output,
|
|
3181
2489
|
nodes: Object.entries(workflowNodes).map(([nodeId, node]) => ({
|
|
@@ -3194,8 +2502,38 @@ function buildWorkflowDetail(name, workflow, workingDir) {
|
|
|
3194
2502
|
json: node.json,
|
|
3195
2503
|
routes: getWorkflowNodeRoutes(node),
|
|
3196
2504
|
})),
|
|
2505
|
+
graph,
|
|
3197
2506
|
};
|
|
3198
2507
|
}
|
|
2508
|
+
export function showWorkflowGraph(config, workflowName, options = {}) {
|
|
2509
|
+
const workflow = config.workflows?.[workflowName];
|
|
2510
|
+
if (!workflow) {
|
|
2511
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
2512
|
+
}
|
|
2513
|
+
const graph = buildWorkflowGraph(workflowName, workflow);
|
|
2514
|
+
const format = options.format ?? 'text';
|
|
2515
|
+
if (format === 'json') {
|
|
2516
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
2517
|
+
}
|
|
2518
|
+
else if (format === 'mermaid') {
|
|
2519
|
+
console.log(formatWorkflowGraphMermaid(graph));
|
|
2520
|
+
}
|
|
2521
|
+
else {
|
|
2522
|
+
console.log(chalk.bold(`\nWorkflow Graph: ${graph.workflow}\n`));
|
|
2523
|
+
for (const node of graph.nodes) {
|
|
2524
|
+
const outgoing = graph.edges.filter((edge) => edge.source === node.id);
|
|
2525
|
+
const suffix = node.condition ? chalk.yellow(` if ${node.condition}`) : '';
|
|
2526
|
+
console.log(` ${chalk.white(node.id)} ${chalk.gray(`[${node.kind}]`)}${suffix}`);
|
|
2527
|
+
for (const edge of outgoing) {
|
|
2528
|
+
const edgeLabel = edge.label ? ` ${chalk.gray(`(${edge.label})`)}` : '';
|
|
2529
|
+
const marker = edge.kind === 'control' ? chalk.cyan('~>') : chalk.gray('->');
|
|
2530
|
+
console.log(` ${marker} ${edge.target}${edgeLabel}`);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
console.log('');
|
|
2534
|
+
}
|
|
2535
|
+
return graph;
|
|
2536
|
+
}
|
|
3199
2537
|
export function showWorkflow(config, workflowName, options = {}) {
|
|
3200
2538
|
const workflow = config.workflows?.[workflowName];
|
|
3201
2539
|
if (!workflow) {
|
|
@@ -3216,6 +2554,9 @@ export function showWorkflow(config, workflowName, options = {}) {
|
|
|
3216
2554
|
if (detail.output) {
|
|
3217
2555
|
console.log(` Output: ${detail.output}`);
|
|
3218
2556
|
}
|
|
2557
|
+
if (detail.metadata) {
|
|
2558
|
+
console.log(` Metadata: ${JSON.stringify(detail.metadata)}`);
|
|
2559
|
+
}
|
|
3219
2560
|
console.log(chalk.bold('\nInputs:'));
|
|
3220
2561
|
const inputEntries = Object.entries(detail.inputs);
|
|
3221
2562
|
if (inputEntries.length === 0) {
|
|
@@ -3306,4 +2647,15 @@ export function validateWorkflows(config, workflowName, options = {}) {
|
|
|
3306
2647
|
console.log('');
|
|
3307
2648
|
return results;
|
|
3308
2649
|
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Default in-process {@link WorkflowExecutor}. Delegates to {@link runWorkflow}.
|
|
2652
|
+
*
|
|
2653
|
+
* The Temporal executor will implement the same interface so `drs workflow run`
|
|
2654
|
+
* can dispatch through either backend once `--executor temporal` is added.
|
|
2655
|
+
*/
|
|
2656
|
+
export class LocalWorkflowExecutor {
|
|
2657
|
+
async run(config, workflowName, options = {}) {
|
|
2658
|
+
return runWorkflow(config, workflowName, options);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
3309
2661
|
//# sourceMappingURL=workflow.js.map
|