@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.
Files changed (139) hide show
  1. package/.pi/agents/review/unified-reviewer.md +2 -6
  2. package/.pi/agents/task/review-assistant.md +22 -0
  3. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +10 -4
  4. package/.pi/workflows/github-pr-review-post.yaml +0 -1
  5. package/.pi/workflows/github-pr-review.yaml +14 -8
  6. package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
  7. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +10 -4
  8. package/.pi/workflows/gitlab-mr-review.yaml +14 -8
  9. package/.pi/workflows/gitlab-mr-visual-explain.yaml +46 -1
  10. package/.pi/workflows/local-fix-review-issues.yaml +13 -6
  11. package/.pi/workflows/local-review.yaml +7 -1
  12. package/.pi/workflows/local-visual-explain.yaml +46 -1
  13. package/.pi/workflows/temporal-control-smoke.yaml +63 -0
  14. package/README.md +39 -0
  15. package/dist/cli/index.js +211 -4
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/init.d.ts +6 -1
  18. package/dist/cli/init.d.ts.map +1 -1
  19. package/dist/cli/init.js +29 -10
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/run-agent.d.ts +2 -7
  22. package/dist/cli/run-agent.d.ts.map +1 -1
  23. package/dist/cli/run-agent.js.map +1 -1
  24. package/dist/cli/workflow-executor-selection.d.ts +3 -0
  25. package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
  26. package/dist/cli/workflow-executor-selection.js +15 -0
  27. package/dist/cli/workflow-executor-selection.js.map +1 -0
  28. package/dist/cli/workflow.d.ts +38 -42
  29. package/dist/cli/workflow.d.ts.map +1 -1
  30. package/dist/cli/workflow.js +190 -838
  31. package/dist/cli/workflow.js.map +1 -1
  32. package/dist/lib/agent-result.d.ts +8 -0
  33. package/dist/lib/agent-result.d.ts.map +1 -0
  34. package/dist/lib/agent-result.js +2 -0
  35. package/dist/lib/agent-result.js.map +1 -0
  36. package/dist/lib/config.d.ts +21 -0
  37. package/dist/lib/config.d.ts.map +1 -1
  38. package/dist/lib/config.js.map +1 -1
  39. package/dist/lib/output-paths.d.ts +0 -1
  40. package/dist/lib/output-paths.d.ts.map +1 -1
  41. package/dist/lib/output-paths.js +0 -1
  42. package/dist/lib/output-paths.js.map +1 -1
  43. package/dist/lib/project-setup.d.ts +10 -0
  44. package/dist/lib/project-setup.d.ts.map +1 -0
  45. package/dist/lib/project-setup.js +25 -0
  46. package/dist/lib/project-setup.js.map +1 -0
  47. package/dist/lib/review-artifact-store.d.ts +16 -0
  48. package/dist/lib/review-artifact-store.d.ts.map +1 -0
  49. package/dist/lib/review-artifact-store.js +84 -0
  50. package/dist/lib/review-artifact-store.js.map +1 -0
  51. package/dist/lib/review-artifact.d.ts.map +1 -1
  52. package/dist/lib/review-artifact.js +1 -0
  53. package/dist/lib/review-artifact.js.map +1 -1
  54. package/dist/lib/review-core.d.ts +1 -0
  55. package/dist/lib/review-core.d.ts.map +1 -1
  56. package/dist/lib/review-core.js +11 -14
  57. package/dist/lib/review-core.js.map +1 -1
  58. package/dist/lib/review-parser.d.ts.map +1 -1
  59. package/dist/lib/review-parser.js +15 -12
  60. package/dist/lib/review-parser.js.map +1 -1
  61. package/dist/lib/skills.d.ts +16 -0
  62. package/dist/lib/skills.d.ts.map +1 -0
  63. package/dist/lib/skills.js +140 -0
  64. package/dist/lib/skills.js.map +1 -0
  65. package/dist/lib/workflow/artifact-store.d.ts +71 -0
  66. package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
  67. package/dist/lib/workflow/artifact-store.js +121 -0
  68. package/dist/lib/workflow/artifact-store.js.map +1 -0
  69. package/dist/lib/workflow/compiled-plan.d.ts +87 -0
  70. package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
  71. package/dist/lib/workflow/compiled-plan.js +62 -0
  72. package/dist/lib/workflow/compiled-plan.js.map +1 -0
  73. package/dist/lib/workflow/executor.d.ts +18 -0
  74. package/dist/lib/workflow/executor.d.ts.map +1 -0
  75. package/dist/lib/workflow/executor.js +2 -0
  76. package/dist/lib/workflow/executor.js.map +1 -0
  77. package/dist/lib/workflow/graph.d.ts +30 -0
  78. package/dist/lib/workflow/graph.d.ts.map +1 -0
  79. package/dist/lib/workflow/graph.js +139 -0
  80. package/dist/lib/workflow/graph.js.map +1 -0
  81. package/dist/lib/workflow/input.d.ts +4 -0
  82. package/dist/lib/workflow/input.d.ts.map +1 -0
  83. package/dist/lib/workflow/input.js +4 -0
  84. package/dist/lib/workflow/input.js.map +1 -0
  85. package/dist/lib/workflow/node-executor.d.ts +19 -0
  86. package/dist/lib/workflow/node-executor.d.ts.map +1 -0
  87. package/dist/lib/workflow/node-executor.js +2 -0
  88. package/dist/lib/workflow/node-executor.js.map +1 -0
  89. package/dist/lib/workflow/planning.d.ts +51 -0
  90. package/dist/lib/workflow/planning.d.ts.map +1 -0
  91. package/dist/lib/workflow/planning.js +814 -0
  92. package/dist/lib/workflow/planning.js.map +1 -0
  93. package/dist/lib/workflow/types.d.ts +66 -0
  94. package/dist/lib/workflow/types.d.ts.map +1 -0
  95. package/dist/lib/workflow/types.js +2 -0
  96. package/dist/lib/workflow/types.js.map +1 -0
  97. package/dist/lib/write-json-output.d.ts.map +1 -1
  98. package/dist/lib/write-json-output.js +3 -8
  99. package/dist/lib/write-json-output.js.map +1 -1
  100. package/dist/pi/sdk.d.ts.map +1 -1
  101. package/dist/pi/sdk.js +6 -2
  102. package/dist/pi/sdk.js.map +1 -1
  103. package/dist/runtime/built-in-paths.d.ts +1 -0
  104. package/dist/runtime/built-in-paths.d.ts.map +1 -1
  105. package/dist/runtime/built-in-paths.js +7 -0
  106. package/dist/runtime/built-in-paths.js.map +1 -1
  107. package/dist/temporal/activities.d.ts +26 -0
  108. package/dist/temporal/activities.d.ts.map +1 -0
  109. package/dist/temporal/activities.js +219 -0
  110. package/dist/temporal/activities.js.map +1 -0
  111. package/dist/temporal/config.d.ts +4 -0
  112. package/dist/temporal/config.d.ts.map +1 -0
  113. package/dist/temporal/config.js +36 -0
  114. package/dist/temporal/config.js.map +1 -0
  115. package/dist/temporal/executor.d.ts +7 -0
  116. package/dist/temporal/executor.d.ts.map +1 -0
  117. package/dist/temporal/executor.js +233 -0
  118. package/dist/temporal/executor.js.map +1 -0
  119. package/dist/temporal/retry-policy.d.ts +28 -0
  120. package/dist/temporal/retry-policy.d.ts.map +1 -0
  121. package/dist/temporal/retry-policy.js +56 -0
  122. package/dist/temporal/retry-policy.js.map +1 -0
  123. package/dist/temporal/types.d.ts +86 -0
  124. package/dist/temporal/types.d.ts.map +1 -0
  125. package/dist/temporal/types.js +2 -0
  126. package/dist/temporal/types.js.map +1 -0
  127. package/dist/temporal/worker.d.ts +3 -0
  128. package/dist/temporal/worker.d.ts.map +1 -0
  129. package/dist/temporal/worker.js +18 -0
  130. package/dist/temporal/worker.js.map +1 -0
  131. package/dist/temporal/workflow-id.d.ts +27 -0
  132. package/dist/temporal/workflow-id.d.ts.map +1 -0
  133. package/dist/temporal/workflow-id.js +53 -0
  134. package/dist/temporal/workflow-id.js.map +1 -0
  135. package/dist/temporal/workflows.d.ts +6 -0
  136. package/dist/temporal/workflows.d.ts.map +1 -0
  137. package/dist/temporal/workflows.js +342 -0
  138. package/dist/temporal/workflows.js.map +1 -0
  139. package/package.json +6 -1
@@ -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 marker = getStringActionOption(node, 'marker', context)?.trim();
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 artifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
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
- let artifactResponse = '';
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
- async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeIds, options, workingDir, context, executionContext) {
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 { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
2126
+ return {
2127
+ nodeId,
2128
+ node,
2129
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2130
+ };
2807
2131
  }
2808
2132
  logWorkflowNodeRunning(nodeId, options);
2809
- const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
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 runControlWorkflowNode(nodeId, node, context) {
2826
- if (node.control === 'loop') {
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 runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext) {
2937
- const segments = splitWorkflowSegments(workflowNodes, executionOrder);
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(config, workflowNodes, segment.nodeIds, segment.activeNodeIds, options, workingDir, context, executionContext);
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 { result, nextNodeId, ended } = skipReason
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 executionOrder = getWorkflowExecutionOrder(workflowNodes);
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 (hasWorkflowControlNodes(workflowNodes)) {
3029
- await runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext);
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 { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
2342
+ return {
2343
+ nodeId,
2344
+ node,
2345
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2346
+ };
3042
2347
  }
3043
2348
  logWorkflowNodeRunning(nodeId, options);
3044
- const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
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 = workflow.output ?? lastNode.output ?? lastNodeId;
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