@diff-review-system/drs 4.0.1 → 5.0.0-rc.1
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/okf-wiki-maintainer.md +72 -0
- package/.pi/agents/task/review-assistant.md +22 -0
- package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +12 -4
- package/.pi/workflows/github-pr-review-post.yaml +29 -20
- package/.pi/workflows/github-pr-review.yaml +37 -8
- package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +2 -0
- package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
- package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +12 -4
- package/.pi/workflows/gitlab-mr-review.yaml +20 -8
- package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +2 -0
- 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/release-changelog-finalize.yaml +10 -0
- package/.pi/workflows/repository-wiki-check.yaml +40 -0
- package/.pi/workflows/repository-wiki-sync.yaml +187 -0
- package/.pi/workflows/temporal-control-smoke.yaml +63 -0
- package/.wiki-site/.vitepress/config.mts +452 -0
- package/.wiki-site/.vitepress/theme/PageLead.vue +122 -0
- package/.wiki-site/.vitepress/theme/index.ts +12 -0
- package/.wiki-site/.vitepress/theme/style.css +270 -0
- package/README.md +77 -4
- package/dist/cli/index.js +87 -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 +26 -10
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run-agent.d.ts +5 -7
- package/dist/cli/run-agent.d.ts.map +1 -1
- package/dist/cli/run-agent.js +56 -5
- package/dist/cli/run-agent.js.map +1 -1
- package/dist/cli/wiki.d.ts +3 -0
- package/dist/cli/wiki.d.ts.map +1 -0
- package/dist/cli/wiki.js +174 -0
- package/dist/cli/wiki.js.map +1 -0
- 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 +40 -42
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +533 -868
- package/dist/cli/workflow.js.map +1 -1
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +2 -2
- package/dist/github/client.js.map +1 -1
- package/dist/github/platform-adapter.d.ts.map +1 -1
- package/dist/github/platform-adapter.js +15 -1
- package/dist/github/platform-adapter.js.map +1 -1
- package/dist/gitlab/client.d.ts +4 -0
- package/dist/gitlab/client.d.ts.map +1 -1
- package/dist/gitlab/client.js +17 -1
- package/dist/gitlab/client.js.map +1 -1
- package/dist/gitlab/platform-adapter.d.ts.map +1 -1
- package/dist/gitlab/platform-adapter.js +17 -1
- package/dist/gitlab/platform-adapter.js.map +1 -1
- package/dist/lib/agent-permissions.d.ts +44 -0
- package/dist/lib/agent-permissions.d.ts.map +1 -0
- package/dist/lib/agent-permissions.js +358 -0
- package/dist/lib/agent-permissions.js.map +1 -0
- package/dist/lib/agent-result.d.ts +10 -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/comment-poster.d.ts +1 -1
- package/dist/lib/comment-poster.d.ts.map +1 -1
- package/dist/lib/comment-poster.js +38 -16
- package/dist/lib/comment-poster.js.map +1 -1
- package/dist/lib/config.d.ts +27 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +17 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/error-comment-poster.d.ts +1 -1
- package/dist/lib/error-comment-poster.d.ts.map +1 -1
- package/dist/lib/error-comment-poster.js +15 -7
- package/dist/lib/error-comment-poster.js.map +1 -1
- package/dist/lib/okf-wiki.d.ts +65 -0
- package/dist/lib/okf-wiki.d.ts.map +1 -0
- package/dist/lib/okf-wiki.js +647 -0
- package/dist/lib/okf-wiki.js.map +1 -0
- 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/platform-client.d.ts +2 -0
- package/dist/lib/platform-client.d.ts.map +1 -1
- package/dist/lib/project-setup.d.ts +7 -0
- package/dist/lib/project-setup.d.ts.map +1 -0
- package/dist/lib/project-setup.js +11 -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 +11 -1
- package/dist/lib/review-artifact.d.ts.map +1 -1
- package/dist/lib/review-artifact.js +203 -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-orchestrator.d.ts +6 -1
- package/dist/lib/review-orchestrator.d.ts.map +1 -1
- package/dist/lib/review-orchestrator.js +3 -1
- package/dist/lib/review-orchestrator.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/wiki-delta.d.ts +71 -0
- package/dist/lib/wiki-delta.d.ts.map +1 -0
- package/dist/lib/wiki-delta.js +524 -0
- package/dist/lib/wiki-delta.js.map +1 -0
- package/dist/lib/wiki-run-summary.d.ts +45 -0
- package/dist/lib/wiki-run-summary.d.ts.map +1 -0
- package/dist/lib/wiki-run-summary.js +168 -0
- package/dist/lib/wiki-run-summary.js.map +1 -0
- package/dist/lib/wiki-search.d.ts +24 -0
- package/dist/lib/wiki-search.d.ts.map +1 -0
- package/dist/lib/wiki-search.js +200 -0
- package/dist/lib/wiki-search.js.map +1 -0
- package/dist/lib/wiki-site-graph.d.ts +33 -0
- package/dist/lib/wiki-site-graph.d.ts.map +1 -0
- package/dist/lib/wiki-site-graph.js +446 -0
- package/dist/lib/wiki-site-graph.js.map +1 -0
- package/dist/lib/wiki-site-safety.d.ts +6 -0
- package/dist/lib/wiki-site-safety.d.ts.map +1 -0
- package/dist/lib/wiki-site-safety.js +83 -0
- package/dist/lib/wiki-site-safety.js.map +1 -0
- package/dist/lib/wiki-site-smoke.d.ts +16 -0
- package/dist/lib/wiki-site-smoke.d.ts.map +1 -0
- package/dist/lib/wiki-site-smoke.js +270 -0
- package/dist/lib/wiki-site-smoke.js.map +1 -0
- package/dist/lib/wiki-site.d.ts +26 -0
- package/dist/lib/wiki-site.d.ts.map +1 -0
- package/dist/lib/wiki-site.js +221 -0
- package/dist/lib/wiki-site.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 +52 -0
- package/dist/lib/workflow/planning.d.ts.map +1 -0
- package/dist/lib/workflow/planning.js +865 -0
- package/dist/lib/workflow/planning.js.map +1 -0
- package/dist/lib/workflow/types.d.ts +71 -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/workflow-artifacts.d.ts.map +1 -1
- package/dist/lib/workflow-artifacts.js +23 -1
- package/dist/lib/workflow-artifacts.js.map +1 -1
- 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 +276 -19
- package/dist/pi/sdk.js.map +1 -1
- package/dist/runtime/client.d.ts +3 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +4 -0
- package/dist/runtime/client.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 +240 -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 +61 -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 +355 -0
- package/dist/temporal/workflows.js.map +1 -0
- package/package.json +24 -9
package/dist/cli/workflow.js
CHANGED
|
@@ -16,15 +16,23 @@ import { resolveCursorFixLinkOptions } from '../lib/cursor-fix-link.js';
|
|
|
16
16
|
import { extractHtmlDocument, parseArtifactOutputPointer, readArtifactOutputPointer, validateHtmlArtifact, } from '../lib/html-artifact.js';
|
|
17
17
|
import { getCanonicalDiffCommand, resolveBaseBranch } from '../lib/repository-validator.js';
|
|
18
18
|
import { formatCodeQualityReport, generateCodeQualityReport } from '../lib/code-quality-report.js';
|
|
19
|
+
import { formatOkfValidationErrors, synchronizeOkfIndexes, validateOkfBundle, } from '../lib/okf-wiki.js';
|
|
20
|
+
import { checkWikiClean, planWikiUpdate, recordWikiState, } from '../lib/wiki-delta.js';
|
|
21
|
+
import { createWikiRunSummary, formatWikiRunSummaryHuman, formatWikiRunSummaryMarkdown, getWikiRunSummary, } from '../lib/wiki-run-summary.js';
|
|
19
22
|
import { createGitHubClient } from '../github/client.js';
|
|
20
23
|
import { GitHubPlatformAdapter } from '../github/platform-adapter.js';
|
|
21
24
|
import { createGitLabClient } from '../gitlab/client.js';
|
|
22
25
|
import { GitLabPlatformAdapter } from '../gitlab/platform-adapter.js';
|
|
23
26
|
import { loadWorkflowArtifact, saveWorkflowArtifact, updateWorkflowArtifact, workflowArtifactExists, } from '../lib/workflow-artifacts.js';
|
|
24
|
-
import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
|
|
27
|
+
import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, reviewArtifactToReviewResult, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
|
|
28
|
+
import { renderAgentPermissions, renderAgentValidation } from '../lib/agent-permissions.js';
|
|
25
29
|
import { runAgent } from './run-agent.js';
|
|
26
30
|
import { TraceCollector } from '../lib/trace-collector.js';
|
|
27
31
|
import { renderTraceHtml } from '../lib/trace-html.js';
|
|
32
|
+
import { buildWorkflowGraph, formatWorkflowGraphMermaid, } from '../lib/workflow/graph.js';
|
|
33
|
+
import { computeActiveWorkflowNodes, createSkippedWorkflowNodeResult, findWorkflowSegmentIndex, getNodeKind, getNodeNeeds, getWorkflowExecutionOrder, getWorkflowExecutionWaves, getWorkflowNodeSkipReason, getWorkflowNodes, hasWorkflowControlNodes, isPotentialWorkspaceMutation, normalizeWorkflowBooleanLike, renderTemplate, runControlWorkflowNode, splitWorkflowSegments, } from '../lib/workflow/planning.js';
|
|
34
|
+
import { getWorkflowInputConfigType } from '../lib/workflow/input.js';
|
|
35
|
+
export { compileWorkflowPlan, } from '../lib/workflow/compiled-plan.js';
|
|
28
36
|
function createWorkflowLock() {
|
|
29
37
|
return { current: Promise.resolve() };
|
|
30
38
|
}
|
|
@@ -80,425 +88,6 @@ function getWorkflowPlatformClient(executionContext, platform) {
|
|
|
80
88
|
executionContext.platformClients[platform] = client;
|
|
81
89
|
return client;
|
|
82
90
|
}
|
|
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
91
|
async function flushWorkflowTrace(traceCollector, workflowName, inputs, startedAt, workingDir, options) {
|
|
503
92
|
const workflowTrace = traceCollector.buildWorkflowTrace(workflowName, inputs, startedAt);
|
|
504
93
|
const scope = {
|
|
@@ -540,9 +129,6 @@ async function resolveWorkflowInput(key, input, workingDir) {
|
|
|
540
129
|
}
|
|
541
130
|
return '';
|
|
542
131
|
}
|
|
543
|
-
function getWorkflowInputConfigType(input) {
|
|
544
|
-
return typeof input === 'string' ? 'string' : (input.type ?? 'string');
|
|
545
|
-
}
|
|
546
132
|
function validateResolvedWorkflowInput(key, input, value) {
|
|
547
133
|
if (typeof input === 'string') {
|
|
548
134
|
return;
|
|
@@ -600,24 +186,11 @@ function resolveAgentsFrom(config, agentsFrom) {
|
|
|
600
186
|
}
|
|
601
187
|
throw new Error(`Unsupported workflow agentsFrom "${agentsFrom}". ` + 'Currently supported: review.agents.');
|
|
602
188
|
}
|
|
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
189
|
function hasConfiguredAgentPrompt(config, agentId) {
|
|
617
190
|
const runConfig = resolveAgentRunConfig(config, agentId);
|
|
618
191
|
return runConfig.prompt !== undefined || runConfig.promptFile !== undefined;
|
|
619
192
|
}
|
|
620
|
-
function createAgentOptions(prompt, options, workingDir) {
|
|
193
|
+
function createAgentOptions(prompt, options, workingDir, node, context) {
|
|
621
194
|
return {
|
|
622
195
|
prompt,
|
|
623
196
|
jsonOutput: false,
|
|
@@ -627,6 +200,16 @@ function createAgentOptions(prompt, options, workingDir) {
|
|
|
627
200
|
quiet: true,
|
|
628
201
|
allowImplicitStdin: false,
|
|
629
202
|
ignoreConfiguredOutput: true,
|
|
203
|
+
...(node.permissions
|
|
204
|
+
? {
|
|
205
|
+
permissions: renderAgentPermissions(node.permissions, (value) => renderTemplate(value, context)),
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
208
|
+
...(node.validation
|
|
209
|
+
? {
|
|
210
|
+
validation: renderAgentValidation(node.validation, (value) => renderTemplate(value, context)),
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
630
213
|
};
|
|
631
214
|
}
|
|
632
215
|
async function writeWorkflowFile(workingDir, relativeOutputPath, content) {
|
|
@@ -686,7 +269,7 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
|
|
|
686
269
|
throw new Error(`Workflow agent node "${nodeId}" must define input or configure ` +
|
|
687
270
|
`agents.overrides.${agentId}.run.prompt/promptFile.`);
|
|
688
271
|
}
|
|
689
|
-
const agentOptions = createAgentOptions(prompt, options, workingDir);
|
|
272
|
+
const agentOptions = createAgentOptions(prompt, options, workingDir, node, context);
|
|
690
273
|
if (executionContext?.traceCollector && prompt) {
|
|
691
274
|
agentOptions.traceCollector = executionContext.traceCollector;
|
|
692
275
|
executionContext.traceCollector.setContext(nodeId, agentId, prompt);
|
|
@@ -704,6 +287,8 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
|
|
|
704
287
|
type: 'agent',
|
|
705
288
|
agent: agentId,
|
|
706
289
|
response: result.response,
|
|
290
|
+
usage: result.usage,
|
|
291
|
+
workspaceChanges: result.workspaceChanges,
|
|
707
292
|
output,
|
|
708
293
|
writes,
|
|
709
294
|
};
|
|
@@ -722,7 +307,7 @@ async function runAgentsWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
722
307
|
`agents.overrides.${missingPromptAgent}.run.prompt/promptFile.`);
|
|
723
308
|
}
|
|
724
309
|
}
|
|
725
|
-
const responses = await Promise.all(agentIds.map((agentId) => runAgent(config, agentId, createAgentOptions(prompt, options, workingDir))));
|
|
310
|
+
const responses = await Promise.all(agentIds.map((agentId) => runAgent(config, agentId, createAgentOptions(prompt, options, workingDir, node, context))));
|
|
726
311
|
const response = responses
|
|
727
312
|
.map((result) => `## ${result.agent}\n\n${result.response.trim()}`.trim())
|
|
728
313
|
.join('\n\n');
|
|
@@ -823,6 +408,27 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
823
408
|
if (node.action === 'code-quality-report') {
|
|
824
409
|
return runCodeQualityReportWorkflowNode(nodeId, node, workingDir, context);
|
|
825
410
|
}
|
|
411
|
+
if (node.action === 'plan-wiki-update') {
|
|
412
|
+
return runPlanWikiUpdateWorkflowNode(nodeId, node, workingDir, context);
|
|
413
|
+
}
|
|
414
|
+
if (node.action === 'sync-okf-indexes') {
|
|
415
|
+
return runSyncOkfIndexesWorkflowNode(nodeId, node, workingDir, context);
|
|
416
|
+
}
|
|
417
|
+
if (node.action === 'validate-okf-wiki') {
|
|
418
|
+
return runValidateOkfWikiWorkflowNode(nodeId, node, workingDir, context);
|
|
419
|
+
}
|
|
420
|
+
if (node.action === 'summarize-wiki-run') {
|
|
421
|
+
return runSummarizeWikiRunWorkflowNode(nodeId, node, context);
|
|
422
|
+
}
|
|
423
|
+
if (node.action === 'record-wiki-state') {
|
|
424
|
+
return runRecordWikiStateWorkflowNode(nodeId, node, workingDir, context);
|
|
425
|
+
}
|
|
426
|
+
if (node.action === 'check-wiki-state') {
|
|
427
|
+
return runCheckWikiStateWorkflowNode(nodeId, node, workingDir, context);
|
|
428
|
+
}
|
|
429
|
+
if (node.action === 'check-wiki-clean') {
|
|
430
|
+
return runCheckWikiCleanWorkflowNode(nodeId, node, workingDir, context);
|
|
431
|
+
}
|
|
826
432
|
if (node.action === 'post-comment') {
|
|
827
433
|
return runPostCommentWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
|
|
828
434
|
}
|
|
@@ -976,13 +582,45 @@ async function runGitBranchWorkflowNode(nodeId, node, workingDir, context, execu
|
|
|
976
582
|
async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
977
583
|
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
978
584
|
const message = requireStringActionOption(nodeId, node, 'message', context);
|
|
585
|
+
const useChangeRequestAuthor = getBooleanActionOption(node, 'useChangeRequestAuthor', context);
|
|
586
|
+
let commitGit = git;
|
|
587
|
+
if (useChangeRequestAuthor) {
|
|
588
|
+
const configuredSource = getStringActionOption(node, 'source', context)?.trim();
|
|
589
|
+
const sourceArtifact = configuredSource ?? 'change';
|
|
590
|
+
const source = context.artifacts[sourceArtifact];
|
|
591
|
+
const platform = isReviewSource(source) && typeof source.context.platform === 'string'
|
|
592
|
+
? source.context.platform
|
|
593
|
+
: undefined;
|
|
594
|
+
const pullRequest = isReviewSource(source) && isPullRequest(source.context.pullRequest)
|
|
595
|
+
? source.context.pullRequest
|
|
596
|
+
: undefined;
|
|
597
|
+
const name = pullRequest?.author.trim();
|
|
598
|
+
const email = pullRequest?.authorEmail?.trim();
|
|
599
|
+
if (!isWorkflowPlatform(platform) ||
|
|
600
|
+
!name ||
|
|
601
|
+
name === 'Unknown' ||
|
|
602
|
+
/[\0\r\n]/.test(name) ||
|
|
603
|
+
!email ||
|
|
604
|
+
/[\0\r\n]/.test(email) ||
|
|
605
|
+
!/^[^\s<>@]+@[^\s<>@]+$/.test(email)) {
|
|
606
|
+
throw new Error(`Workflow git-commit node "${nodeId}" with.useChangeRequestAuthor requires source artifact "${sourceArtifact}" to contain a GitHub PR or GitLab MR creator identity.`);
|
|
607
|
+
}
|
|
608
|
+
const environment = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
609
|
+
commitGit = simpleGit({ baseDir: workingDir }).env({
|
|
610
|
+
...environment,
|
|
611
|
+
GIT_AUTHOR_NAME: name,
|
|
612
|
+
GIT_AUTHOR_EMAIL: email,
|
|
613
|
+
GIT_COMMITTER_NAME: name,
|
|
614
|
+
GIT_COMMITTER_EMAIL: email,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
979
617
|
const paths = hasActionOption(node, 'paths') || hasActionOption(node, 'path')
|
|
980
618
|
? getPathActionOption(nodeId, node, context, workingDir)
|
|
981
619
|
: undefined;
|
|
982
620
|
if (paths) {
|
|
983
621
|
await git.add(paths);
|
|
984
622
|
}
|
|
985
|
-
const commit = paths ? await
|
|
623
|
+
const commit = paths ? await commitGit.commit(message, paths) : await commitGit.commit(message);
|
|
986
624
|
const output = {
|
|
987
625
|
commit: commit.commit,
|
|
988
626
|
message,
|
|
@@ -1555,16 +1193,59 @@ function createPlatformChangeSource(platform, name, projectId, pullRequest, chan
|
|
|
1555
1193
|
workingDir,
|
|
1556
1194
|
};
|
|
1557
1195
|
}
|
|
1196
|
+
function getPatchChangeCounts(patch) {
|
|
1197
|
+
let additions = 0;
|
|
1198
|
+
let deletions = 0;
|
|
1199
|
+
for (const line of patch.split('\n')) {
|
|
1200
|
+
if (line.startsWith('+'))
|
|
1201
|
+
additions += 1;
|
|
1202
|
+
if (line.startsWith('-'))
|
|
1203
|
+
deletions += 1;
|
|
1204
|
+
}
|
|
1205
|
+
return { additions, deletions };
|
|
1206
|
+
}
|
|
1558
1207
|
async function loadGitHubChangeSource(nodeId, node, workingDir, context, executionContext) {
|
|
1559
1208
|
const owner = requireStringActionOption(nodeId, node, 'owner', context);
|
|
1560
1209
|
const repo = requireStringActionOption(nodeId, node, 'repo', context);
|
|
1561
1210
|
const prNumber = requireNumberActionOption(nodeId, node, 'pr', context);
|
|
1562
1211
|
const projectId = `${owner}/${repo}`;
|
|
1563
1212
|
const platformClient = getWorkflowPlatformClient(executionContext, 'github');
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1213
|
+
const requireCompleteDiff = getBooleanActionOption(node, 'requireCompleteDiff', context);
|
|
1214
|
+
let pullRequest;
|
|
1215
|
+
let changedFiles;
|
|
1216
|
+
if (requireCompleteDiff) {
|
|
1217
|
+
const before = await platformClient.getPullRequest(projectId, prNumber);
|
|
1218
|
+
changedFiles = await platformClient.getChangedFiles(projectId, prNumber);
|
|
1219
|
+
const after = await platformClient.getPullRequest(projectId, prNumber);
|
|
1220
|
+
if (before.headSha !== after.headSha) {
|
|
1221
|
+
throw new Error(`Workflow change-source node "${nodeId}" cannot review an unstable pull request head.`);
|
|
1222
|
+
}
|
|
1223
|
+
const platformData = after.platformData;
|
|
1224
|
+
const expectedFileCount = platformData && typeof platformData === 'object' && 'changed_files' in platformData
|
|
1225
|
+
? platformData.changed_files
|
|
1226
|
+
: undefined;
|
|
1227
|
+
if (typeof expectedFileCount !== 'number' ||
|
|
1228
|
+
!Number.isInteger(expectedFileCount) ||
|
|
1229
|
+
expectedFileCount !== changedFiles.length) {
|
|
1230
|
+
throw new Error(`Workflow change-source node "${nodeId}" did not receive the complete GitHub file list.`);
|
|
1231
|
+
}
|
|
1232
|
+
const incompleteFiles = changedFiles.filter((file) => {
|
|
1233
|
+
if (!file.patch || file.patch.length === 0)
|
|
1234
|
+
return true;
|
|
1235
|
+
const counts = getPatchChangeCounts(file.patch);
|
|
1236
|
+
return counts.additions !== file.additions || counts.deletions !== file.deletions;
|
|
1237
|
+
});
|
|
1238
|
+
if (incompleteFiles.length > 0) {
|
|
1239
|
+
throw new Error(`Workflow change-source node "${nodeId}" did not receive complete patches for: ${incompleteFiles.map((file) => file.filename).join(', ')}`);
|
|
1240
|
+
}
|
|
1241
|
+
pullRequest = after;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
[pullRequest, changedFiles] = await Promise.all([
|
|
1245
|
+
platformClient.getPullRequest(projectId, prNumber),
|
|
1246
|
+
platformClient.getChangedFiles(projectId, prNumber),
|
|
1247
|
+
]);
|
|
1248
|
+
}
|
|
1568
1249
|
return createPlatformChangeSource('github', `GitHub PR ${projectId}#${prNumber}`, projectId, pullRequest, changedFiles, workingDir);
|
|
1569
1250
|
}
|
|
1570
1251
|
async function loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext) {
|
|
@@ -1987,6 +1668,154 @@ async function runCodeQualityReportWorkflowNode(nodeId, node, workingDir, contex
|
|
|
1987
1668
|
writes: reportPath,
|
|
1988
1669
|
};
|
|
1989
1670
|
}
|
|
1671
|
+
async function runSyncOkfIndexesWorkflowNode(nodeId, node, workingDir, context) {
|
|
1672
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1673
|
+
const version = getStringActionOption(node, 'version', context)?.trim() ?? '0.1';
|
|
1674
|
+
const result = await synchronizeOkfIndexes(workingDir, root, version);
|
|
1675
|
+
return {
|
|
1676
|
+
id: nodeId,
|
|
1677
|
+
type: 'action',
|
|
1678
|
+
action: node.action,
|
|
1679
|
+
response: `synchronized ${result.indexes} OKF indexes under ${result.root} (${result.updated} updated)`,
|
|
1680
|
+
output: result,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function getWikiInstructionsActionOptions(node, context) {
|
|
1684
|
+
const instructionsPath = getStringActionOption(node, 'instructionsPath', context)?.trim();
|
|
1685
|
+
const instructions = getStringActionOption(node, 'instructions', context);
|
|
1686
|
+
return {
|
|
1687
|
+
...(instructionsPath ? { instructionsPath } : {}),
|
|
1688
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
async function runPlanWikiUpdateWorkflowNode(nodeId, node, workingDir, context) {
|
|
1692
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1693
|
+
const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
|
|
1694
|
+
const result = await planWikiUpdate(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
|
|
1695
|
+
return {
|
|
1696
|
+
id: nodeId,
|
|
1697
|
+
type: 'action',
|
|
1698
|
+
action: node.action,
|
|
1699
|
+
response: `${result.mode}: ${result.reason}`,
|
|
1700
|
+
output: result,
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
async function runValidateOkfWikiWorkflowNode(nodeId, node, workingDir, context) {
|
|
1704
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1705
|
+
const version = getStringActionOption(node, 'version', context)?.trim() ?? '0.1';
|
|
1706
|
+
const result = await validateOkfBundle(workingDir, root, version);
|
|
1707
|
+
if (!result.valid) {
|
|
1708
|
+
throw new Error(`Workflow validate-okf-wiki node "${nodeId}" found ${result.errors.length} error(s):\n${formatOkfValidationErrors(result)}`);
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
id: nodeId,
|
|
1712
|
+
type: 'action',
|
|
1713
|
+
action: node.action,
|
|
1714
|
+
response: `validated ${result.concepts} OKF concepts under ${result.root} with ` +
|
|
1715
|
+
`${result.warnings.length} warning(s); graph: ${result.graph.nodeCount} nodes, ` +
|
|
1716
|
+
`${result.graph.directedEdgeCount} directed edges, ` +
|
|
1717
|
+
`${result.graph.orphanConceptCount} orphans, ` +
|
|
1718
|
+
`${result.graph.weaklyConnectedConceptCount} weakly connected`,
|
|
1719
|
+
output: result,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function runSummarizeWikiRunWorkflowNode(nodeId, node, context) {
|
|
1723
|
+
const planName = getStringActionOption(node, 'plan', context)?.trim() ?? 'wikiDelta';
|
|
1724
|
+
const validationName = getStringActionOption(node, 'validation', context)?.trim() ?? 'wikiValidation';
|
|
1725
|
+
const agentNodeId = getStringActionOption(node, 'agentNode', context)?.trim() ?? 'maintain-wiki';
|
|
1726
|
+
const plan = requireWikiUpdatePlan(context.artifacts[planName], nodeId, planName);
|
|
1727
|
+
const validation = requireWikiValidation(context.artifacts[validationName], nodeId, validationName);
|
|
1728
|
+
const agentNode = context.nodes[agentNodeId];
|
|
1729
|
+
if (plan.shouldRun &&
|
|
1730
|
+
(!agentNode || agentNode.status === 'skipped' || agentNode.type !== 'agent')) {
|
|
1731
|
+
throw new Error(`Workflow summarize-wiki-run node "${nodeId}" expected completed agent node "${agentNodeId}".`);
|
|
1732
|
+
}
|
|
1733
|
+
const startedAt = context.startedAt ? Date.parse(context.startedAt) : Date.now();
|
|
1734
|
+
const summary = createWikiRunSummary({
|
|
1735
|
+
plan,
|
|
1736
|
+
validation,
|
|
1737
|
+
modelInvoked: plan.shouldRun,
|
|
1738
|
+
usage: plan.shouldRun ? agentNode?.usage : undefined,
|
|
1739
|
+
workspaceChanges: plan.shouldRun ? agentNode?.workspaceChanges : undefined,
|
|
1740
|
+
elapsedMs: Date.now() - (Number.isFinite(startedAt) ? startedAt : Date.now()),
|
|
1741
|
+
});
|
|
1742
|
+
const response = formatWikiRunSummaryHuman(summary);
|
|
1743
|
+
return {
|
|
1744
|
+
id: nodeId,
|
|
1745
|
+
type: 'action',
|
|
1746
|
+
action: node.action,
|
|
1747
|
+
response,
|
|
1748
|
+
output: { ...validation, summary, summaryMarkdown: formatWikiRunSummaryMarkdown(summary) },
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function requireWikiUpdatePlan(value, nodeId, artifactName) {
|
|
1752
|
+
if (!isRecord(value) ||
|
|
1753
|
+
!['generate', 'reconcile', 'update', 'noop'].includes(String(value.mode)) ||
|
|
1754
|
+
typeof value.shouldRun !== 'boolean' ||
|
|
1755
|
+
typeof value.changedPathCount !== 'number') {
|
|
1756
|
+
throw new Error(`Workflow summarize-wiki-run node "${nodeId}" needs a wiki update plan artifact at "${artifactName}".`);
|
|
1757
|
+
}
|
|
1758
|
+
return value;
|
|
1759
|
+
}
|
|
1760
|
+
function requireWikiValidation(value, nodeId, artifactName) {
|
|
1761
|
+
if (!isRecord(value) ||
|
|
1762
|
+
typeof value.valid !== 'boolean' ||
|
|
1763
|
+
typeof value.root !== 'string' ||
|
|
1764
|
+
typeof value.concepts !== 'number' ||
|
|
1765
|
+
!Array.isArray(value.errors) ||
|
|
1766
|
+
!Array.isArray(value.warnings) ||
|
|
1767
|
+
!isRecord(value.graph)) {
|
|
1768
|
+
throw new Error(`Workflow summarize-wiki-run node "${nodeId}" needs an OKF validation artifact at "${artifactName}".`);
|
|
1769
|
+
}
|
|
1770
|
+
return value;
|
|
1771
|
+
}
|
|
1772
|
+
function isRecord(value) {
|
|
1773
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1774
|
+
}
|
|
1775
|
+
async function runRecordWikiStateWorkflowNode(nodeId, node, workingDir, context) {
|
|
1776
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1777
|
+
const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
|
|
1778
|
+
const result = await recordWikiState(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
|
|
1779
|
+
return {
|
|
1780
|
+
id: nodeId,
|
|
1781
|
+
type: 'action',
|
|
1782
|
+
action: node.action,
|
|
1783
|
+
response: `recorded wiki state for ${result.root} at ${result.gitHead}`,
|
|
1784
|
+
output: result,
|
|
1785
|
+
writes: statePath,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
async function runCheckWikiCleanWorkflowNode(nodeId, node, workingDir, context) {
|
|
1789
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1790
|
+
const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
|
|
1791
|
+
const result = await checkWikiClean(workingDir, root, statePath);
|
|
1792
|
+
if (!result.clean) {
|
|
1793
|
+
throw new Error(`Workflow check-wiki-clean node "${nodeId}" found stale wiki output:\n${result.changedPaths.map((filePath) => `- ${filePath}`).join('\n')}`);
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
id: nodeId,
|
|
1797
|
+
type: 'action',
|
|
1798
|
+
action: node.action,
|
|
1799
|
+
response: `wiki bundle ${result.root} and state ${result.statePath} are current`,
|
|
1800
|
+
output: result,
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
async function runCheckWikiStateWorkflowNode(nodeId, node, workingDir, context) {
|
|
1804
|
+
const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
|
|
1805
|
+
const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
|
|
1806
|
+
const result = await planWikiUpdate(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
|
|
1807
|
+
if (result.shouldRun) {
|
|
1808
|
+
const paths = result.changedPaths.map((filePath) => `- ${filePath}`).join('\n');
|
|
1809
|
+
throw new Error(`Workflow check-wiki-state node "${nodeId}" found a stale wiki (${result.mode}): ${result.reason}${paths ? `\n${paths}` : ''}`);
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
id: nodeId,
|
|
1813
|
+
type: 'action',
|
|
1814
|
+
action: node.action,
|
|
1815
|
+
response: result.reason,
|
|
1816
|
+
output: result,
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1990
1819
|
async function runDescribeWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
1991
1820
|
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1992
1821
|
const source = context.artifacts[sourceArtifact];
|
|
@@ -2041,7 +1870,10 @@ async function runPostCommentWorkflowNode(nodeId, node, options, workingDir, con
|
|
|
2041
1870
|
const rawBody = node.input === undefined
|
|
2042
1871
|
? requireStringActionOption(nodeId, node, 'body', context)
|
|
2043
1872
|
: renderTemplate(node.input, context);
|
|
2044
|
-
const
|
|
1873
|
+
const configuredMarker = getStringActionOption(node, 'marker', context)?.trim();
|
|
1874
|
+
const marker = configuredMarker && configuredMarker.length > 0
|
|
1875
|
+
? configuredMarker
|
|
1876
|
+
: options.idempotencyContext?.idempotencyKey;
|
|
2045
1877
|
const body = formatMarkedComment(rawBody, marker);
|
|
2046
1878
|
let operation = 'created';
|
|
2047
1879
|
if (marker) {
|
|
@@ -2076,15 +1908,35 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
|
|
|
2076
1908
|
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
2077
1909
|
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
|
|
2078
1910
|
const source = context.artifacts[sourceArtifact];
|
|
2079
|
-
const
|
|
1911
|
+
const reviewValue = context.artifacts[reviewArtifact];
|
|
2080
1912
|
if (!isReviewSource(source)) {
|
|
2081
1913
|
throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewSource artifact.`);
|
|
2082
1914
|
}
|
|
2083
|
-
if (!isReviewResult(reviewResult)) {
|
|
2084
|
-
throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewResult artifact.`);
|
|
2085
|
-
}
|
|
2086
1915
|
const target = resolvePostTarget(nodeId, node, context, executionContext, source);
|
|
2087
1916
|
const pullRequest = target.pullRequest;
|
|
1917
|
+
const canonicalArtifactMode = hasActionOption(node, 'expectedHeadSha');
|
|
1918
|
+
const expectedHeadSha = getStringActionOption(node, 'expectedHeadSha', context)?.trim();
|
|
1919
|
+
let reviewResult;
|
|
1920
|
+
if (canonicalArtifactMode) {
|
|
1921
|
+
if (!expectedHeadSha || !pullRequest) {
|
|
1922
|
+
throw new Error(`Workflow post-review-comments node "${nodeId}" needs pull request metadata and a non-empty expectedHeadSha to validate a canonical review artifact.`);
|
|
1923
|
+
}
|
|
1924
|
+
reviewResult = reviewArtifactToReviewResult(reviewValue, {
|
|
1925
|
+
platform: target.platform,
|
|
1926
|
+
projectId: target.projectId,
|
|
1927
|
+
changeKind: target.platform === 'gitlab' ? 'mr' : 'pr',
|
|
1928
|
+
changeNumber: target.prNumber,
|
|
1929
|
+
expectedHeadSha,
|
|
1930
|
+
currentHeadSha: pullRequest.headSha,
|
|
1931
|
+
changedFiles: source.files,
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
else if (isReviewResult(reviewValue)) {
|
|
1935
|
+
reviewResult = reviewValue;
|
|
1936
|
+
}
|
|
1937
|
+
else {
|
|
1938
|
+
throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewResult artifact or a canonical review artifact with expectedHeadSha.`);
|
|
1939
|
+
}
|
|
2088
1940
|
const platformData = pullRequest?.platformData;
|
|
2089
1941
|
const lineValidator = createWorkflowLineValidator(target.platform, source);
|
|
2090
1942
|
const createInlinePosition = lineValidator
|
|
@@ -2092,10 +1944,15 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
|
|
|
2092
1944
|
: undefined;
|
|
2093
1945
|
const shouldRemoveErrorComment = !hasActionOption(node, 'removeErrorComment') ||
|
|
2094
1946
|
getBooleanActionOption(node, 'removeErrorComment', context);
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
await
|
|
1947
|
+
const assertCurrentHead = expectedHeadSha
|
|
1948
|
+
? async () => {
|
|
1949
|
+
const latestPullRequest = await target.platformClient.getPullRequest(target.projectId, target.prNumber);
|
|
1950
|
+
if (latestPullRequest.headSha !== expectedHeadSha) {
|
|
1951
|
+
throw new Error('Pull request head changed before review comments could be posted.');
|
|
1952
|
+
}
|
|
2098
1953
|
}
|
|
1954
|
+
: undefined;
|
|
1955
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
|
|
2099
1956
|
const cursorFixLinks = resolveCursorFixLinkOptions(config, target.projectId, workingDir);
|
|
2100
1957
|
await postReviewComments(target.platformClient, target.projectId, target.prNumber, reviewResult.summary, reviewResult.issues, reviewResult.changeSummary, reviewResult.usage, platformData, lineValidator, createInlinePosition, cursorFixLinks, pullRequest
|
|
2101
1958
|
? {
|
|
@@ -2103,6 +1960,8 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
|
|
|
2103
1960
|
sourceBranch: pullRequest.sourceBranch,
|
|
2104
1961
|
targetBranch: pullRequest.targetBranch,
|
|
2105
1962
|
}
|
|
1963
|
+
: undefined, assertCurrentHead, shouldRemoveErrorComment
|
|
1964
|
+
? () => removeErrorComment(target.platformClient, target.projectId, target.prNumber, assertCurrentHead)
|
|
2106
1965
|
: undefined);
|
|
2107
1966
|
});
|
|
2108
1967
|
return {
|
|
@@ -2432,11 +2291,17 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
2432
2291
|
console.warn = () => undefined;
|
|
2433
2292
|
}
|
|
2434
2293
|
try {
|
|
2435
|
-
|
|
2294
|
+
const reviewSource = {
|
|
2436
2295
|
...sourceForReview,
|
|
2437
2296
|
workingDir: sourceForReview.workingDir ?? workingDir,
|
|
2438
2297
|
debug: options.debug,
|
|
2439
2298
|
thinkingLevel: options.thinkingLevel,
|
|
2299
|
+
};
|
|
2300
|
+
if (!node.permissions) {
|
|
2301
|
+
return await executeReview(config, reviewSource);
|
|
2302
|
+
}
|
|
2303
|
+
return await executeReview(config, reviewSource, {
|
|
2304
|
+
permissions: renderAgentPermissions(node.permissions, (value) => renderTemplate(value, context)),
|
|
2440
2305
|
});
|
|
2441
2306
|
}
|
|
2442
2307
|
finally {
|
|
@@ -2450,19 +2315,25 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
2450
2315
|
if (writes) {
|
|
2451
2316
|
await writeWorkflowFile(workingDir, writes, JSON.stringify(reviewResult, null, 2));
|
|
2452
2317
|
}
|
|
2453
|
-
const
|
|
2318
|
+
const explicitArtifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
|
|
2319
|
+
let artifactOutput;
|
|
2320
|
+
if (explicitArtifactOutput) {
|
|
2321
|
+
artifactOutput = explicitArtifactOutput;
|
|
2322
|
+
}
|
|
2323
|
+
else if (!reviewArtifactName) {
|
|
2324
|
+
artifactOutput = `${nodeId}Artifact`;
|
|
2325
|
+
}
|
|
2454
2326
|
const outputs = {};
|
|
2455
|
-
|
|
2327
|
+
const reviewArtifactPayload = createReviewArtifactPayload(reviewResult, source);
|
|
2328
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
2329
|
+
const saved = await saveWorkflowArtifact(workingDir, {
|
|
2330
|
+
kind: 'review',
|
|
2331
|
+
scope,
|
|
2332
|
+
payload: reviewArtifactPayload,
|
|
2333
|
+
});
|
|
2334
|
+
const artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
|
|
2456
2335
|
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
2336
|
outputs[artifactOutput] = { ...saved.artifact, path: saved.path, latestPath: saved.latestPath };
|
|
2465
|
-
artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
|
|
2466
2337
|
}
|
|
2467
2338
|
return {
|
|
2468
2339
|
id: nodeId,
|
|
@@ -2486,272 +2357,6 @@ function recordNodeArtifact(nodeId, node, result, artifacts) {
|
|
|
2486
2357
|
}
|
|
2487
2358
|
}
|
|
2488
2359
|
}
|
|
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
2360
|
function logWorkflowNodeRunning(nodeId, options) {
|
|
2756
2361
|
if (!options.jsonOutput) {
|
|
2757
2362
|
console.log(chalk.gray(`Running node ${nodeId}...`));
|
|
@@ -2769,7 +2374,14 @@ function recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts) {
|
|
|
2769
2374
|
recordNodeArtifact(nodeId, node, result, artifacts);
|
|
2770
2375
|
}
|
|
2771
2376
|
}
|
|
2772
|
-
|
|
2377
|
+
function completeWorkflowNodeResult(result, startedAt) {
|
|
2378
|
+
const completedAt = new Date().toISOString();
|
|
2379
|
+
result.startedAt ??= startedAt;
|
|
2380
|
+
result.completedAt ??= completedAt;
|
|
2381
|
+
result.durationMs ??= Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime());
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
async function runWorkflowDagSegment(workflowNodes, nodeIds, activeNodeIds, options, context, nodeExecutor) {
|
|
2773
2385
|
const completed = new Set();
|
|
2774
2386
|
const segmentNodeIds = new Set(nodeIds);
|
|
2775
2387
|
if (activeNodeIds) {
|
|
@@ -2800,14 +2412,19 @@ async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeI
|
|
|
2800
2412
|
if (!node) {
|
|
2801
2413
|
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
2802
2414
|
}
|
|
2415
|
+
const startedAt = new Date().toISOString();
|
|
2803
2416
|
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
2804
2417
|
if (skipReason) {
|
|
2805
2418
|
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
2806
|
-
return {
|
|
2419
|
+
return {
|
|
2420
|
+
nodeId,
|
|
2421
|
+
node,
|
|
2422
|
+
result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
|
|
2423
|
+
};
|
|
2807
2424
|
}
|
|
2808
2425
|
logWorkflowNodeRunning(nodeId, options);
|
|
2809
|
-
const result = await
|
|
2810
|
-
return { nodeId, node, result };
|
|
2426
|
+
const result = await nodeExecutor.runNode(nodeId, node, context);
|
|
2427
|
+
return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
|
|
2811
2428
|
}));
|
|
2812
2429
|
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
2813
2430
|
if (firstRejection) {
|
|
@@ -2822,124 +2439,22 @@ async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeI
|
|
|
2822
2439
|
}
|
|
2823
2440
|
}
|
|
2824
2441
|
}
|
|
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}".`);
|
|
2442
|
+
async function runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor) {
|
|
2443
|
+
await runControlWorkflowFromSegments(workflowNodes, splitWorkflowSegments(workflowNodes, executionOrder), options, context, nodeExecutor);
|
|
2935
2444
|
}
|
|
2936
|
-
async function
|
|
2937
|
-
const segments =
|
|
2445
|
+
async function runControlWorkflowFromSegments(workflowNodes, compiledSegments, options, context, nodeExecutor) {
|
|
2446
|
+
const segments = compiledSegments.map((segment) => segment.type === 'control'
|
|
2447
|
+
? { type: 'control', nodeId: segment.nodeId }
|
|
2448
|
+
: {
|
|
2449
|
+
type: 'dag',
|
|
2450
|
+
nodeIds: [...segment.nodeIds],
|
|
2451
|
+
...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
|
|
2452
|
+
});
|
|
2938
2453
|
let segmentIndex = 0;
|
|
2939
2454
|
while (segmentIndex < segments.length) {
|
|
2940
2455
|
const segment = segments[segmentIndex];
|
|
2941
2456
|
if (segment.type === 'dag') {
|
|
2942
|
-
await runWorkflowDagSegment(
|
|
2457
|
+
await runWorkflowDagSegment(workflowNodes, segment.nodeIds, segment.activeNodeIds, options, context, nodeExecutor);
|
|
2943
2458
|
segmentIndex += 1;
|
|
2944
2459
|
continue;
|
|
2945
2460
|
}
|
|
@@ -2954,9 +2469,14 @@ async function runControlWorkflow(config, workflowNodes, executionOrder, options
|
|
|
2954
2469
|
else {
|
|
2955
2470
|
logWorkflowNodeRunning(segment.nodeId, options);
|
|
2956
2471
|
}
|
|
2957
|
-
const
|
|
2472
|
+
const startedAt = new Date().toISOString();
|
|
2473
|
+
const controlResult = skipReason
|
|
2958
2474
|
? { result: createSkippedWorkflowNodeResult(segment.nodeId) }
|
|
2959
2475
|
: runControlWorkflowNode(segment.nodeId, node, context);
|
|
2476
|
+
const { result, nextNodeId, ended } = {
|
|
2477
|
+
...controlResult,
|
|
2478
|
+
result: completeWorkflowNodeResult(controlResult.result, startedAt),
|
|
2479
|
+
};
|
|
2960
2480
|
recordWorkflowNodeResult(segment.nodeId, node, result, context.nodes, context.artifacts);
|
|
2961
2481
|
if (ended) {
|
|
2962
2482
|
return;
|
|
@@ -2987,16 +2507,57 @@ async function runSingleWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
2987
2507
|
if (getWorkflowNodeSkipReason(node, context)) {
|
|
2988
2508
|
return createSkippedWorkflowNodeResult(nodeId);
|
|
2989
2509
|
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
2510
|
+
const run = async () => {
|
|
2511
|
+
if (kind === 'agent') {
|
|
2512
|
+
return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
2513
|
+
}
|
|
2514
|
+
if (kind === 'agents') {
|
|
2515
|
+
return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
|
|
2516
|
+
}
|
|
2517
|
+
if (kind === 'control') {
|
|
2518
|
+
throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
|
|
2519
|
+
}
|
|
2520
|
+
return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
2521
|
+
};
|
|
2522
|
+
return isPotentialWorkspaceMutation(node)
|
|
2523
|
+
? withWorkflowLock(executionContext.locks.workspace, run)
|
|
2524
|
+
: run();
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* In-process {@link NodeExecutor} that dispatches a single non-control node
|
|
2528
|
+
* to the existing side-effecting runner functions. Constructed once per
|
|
2529
|
+
* workflow run so git/platform clients and locks are shared across nodes.
|
|
2530
|
+
*
|
|
2531
|
+
* The Temporal executor will provide its own NodeExecutor whose `runNode`
|
|
2532
|
+
* schedules a Temporal activity instead of executing in-process.
|
|
2533
|
+
*/
|
|
2534
|
+
class LocalNodeExecutor {
|
|
2535
|
+
config;
|
|
2536
|
+
options;
|
|
2537
|
+
workingDir;
|
|
2538
|
+
executionContext;
|
|
2539
|
+
constructor(config, options, workingDir, executionContext) {
|
|
2540
|
+
this.config = config;
|
|
2541
|
+
this.options = options;
|
|
2542
|
+
this.workingDir = workingDir;
|
|
2543
|
+
this.executionContext = executionContext;
|
|
2544
|
+
}
|
|
2545
|
+
async runNode(nodeId, node, context) {
|
|
2546
|
+
return runSingleWorkflowNode(this.config, nodeId, node, this.options, this.workingDir, context, this.executionContext);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
export async function runWorkflowNodeLocally(config, nodeId, node, options, workingDir, context) {
|
|
2550
|
+
const executionContext = {
|
|
2551
|
+
gitClients: new Map(),
|
|
2552
|
+
platformClients: {},
|
|
2553
|
+
traceCollector: options.trace ? new TraceCollector() : undefined,
|
|
2554
|
+
locks: {
|
|
2555
|
+
exit: createWorkflowLock(),
|
|
2556
|
+
console: createWorkflowLock(),
|
|
2557
|
+
workspace: createWorkflowLock(),
|
|
2558
|
+
},
|
|
2559
|
+
};
|
|
2560
|
+
return runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
3000
2561
|
}
|
|
3001
2562
|
export async function runWorkflow(config, workflowName, options = {}) {
|
|
3002
2563
|
const workflow = config.workflows?.[workflowName];
|
|
@@ -3004,12 +2565,40 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3004
2565
|
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
3005
2566
|
}
|
|
3006
2567
|
const workflowNodes = getWorkflowNodes(workflowName, workflow);
|
|
2568
|
+
const executionOrder = getWorkflowExecutionOrder(workflowNodes);
|
|
2569
|
+
const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
|
|
2570
|
+
return executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflow.output, hasWorkflowControlNodes(workflowNodes), options);
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Run a workflow from a previously compiled {@link CompiledWorkflowPlan}.
|
|
2574
|
+
*
|
|
2575
|
+
* The plan supplies the scheduling data (nodes, execution order, waves,
|
|
2576
|
+
* segments, output key) so the caller does not need to reload repo config to
|
|
2577
|
+
* drive execution. `config` is still required because agent/action node
|
|
2578
|
+
* execution needs agent and review configuration that lives outside the
|
|
2579
|
+
* compiled plan.
|
|
2580
|
+
*/
|
|
2581
|
+
export async function runWorkflowFromCompiledPlan(config, plan, options = {}) {
|
|
2582
|
+
const executionSegments = plan.hasControlNodes ? plan.segments : [];
|
|
2583
|
+
if (plan.hasControlNodes) {
|
|
2584
|
+
const workflowNodes = plan.nodes;
|
|
2585
|
+
const order = plan.executionOrder;
|
|
2586
|
+
return executeWorkflowRun(config, plan.workflowName, workflowNodes, order, plan.waves, plan.output, true, options, executionSegments);
|
|
2587
|
+
}
|
|
2588
|
+
return executeWorkflowRun(config, plan.workflowName, plan.nodes, plan.executionOrder, plan.waves, plan.output, false, options);
|
|
2589
|
+
}
|
|
2590
|
+
async function executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflowOutput, hasControl, options, controlSegments) {
|
|
2591
|
+
const startedAt = new Date().toISOString();
|
|
2592
|
+
const workflow = config.workflows?.[workflowName];
|
|
2593
|
+
if (!workflow) {
|
|
2594
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
2595
|
+
}
|
|
3007
2596
|
const workingDir = options.workingDir ?? process.cwd();
|
|
3008
2597
|
const inputs = await resolveWorkflowInputs(workflow, options, workingDir);
|
|
3009
2598
|
const nodes = {};
|
|
3010
2599
|
const artifacts = {};
|
|
3011
2600
|
const loop = {};
|
|
3012
|
-
const context = { inputs, nodes, artifacts, loop };
|
|
2601
|
+
const context = { startedAt, inputs, nodes, artifacts, loop };
|
|
3013
2602
|
const executionContext = {
|
|
3014
2603
|
gitClients: new Map(),
|
|
3015
2604
|
platformClients: {},
|
|
@@ -3017,16 +2606,28 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3017
2606
|
locks: {
|
|
3018
2607
|
exit: createWorkflowLock(),
|
|
3019
2608
|
console: createWorkflowLock(),
|
|
2609
|
+
workspace: createWorkflowLock(),
|
|
3020
2610
|
},
|
|
3021
2611
|
};
|
|
3022
|
-
const
|
|
3023
|
-
const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
|
|
2612
|
+
const nodeExecutor = new LocalNodeExecutor(config, options, workingDir, executionContext);
|
|
3024
2613
|
if (!options.jsonOutput) {
|
|
3025
2614
|
console.log(chalk.gray(`Running workflow ${workflowName}...\n`));
|
|
3026
2615
|
}
|
|
3027
2616
|
try {
|
|
3028
|
-
if (
|
|
3029
|
-
|
|
2617
|
+
if (hasControl) {
|
|
2618
|
+
if (controlSegments) {
|
|
2619
|
+
const internalSegments = controlSegments.map((segment) => segment.type === 'control'
|
|
2620
|
+
? { type: 'control', nodeId: segment.nodeId }
|
|
2621
|
+
: {
|
|
2622
|
+
type: 'dag',
|
|
2623
|
+
nodeIds: segment.nodeIds,
|
|
2624
|
+
...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
|
|
2625
|
+
});
|
|
2626
|
+
await runControlWorkflowFromSegments(workflowNodes, internalSegments, options, context, nodeExecutor);
|
|
2627
|
+
}
|
|
2628
|
+
else {
|
|
2629
|
+
await runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor);
|
|
2630
|
+
}
|
|
3030
2631
|
}
|
|
3031
2632
|
else {
|
|
3032
2633
|
for (const wave of executionWaves) {
|
|
@@ -3035,14 +2636,19 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3035
2636
|
if (!node) {
|
|
3036
2637
|
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
3037
2638
|
}
|
|
2639
|
+
const startedAt = new Date().toISOString();
|
|
3038
2640
|
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
3039
2641
|
if (skipReason) {
|
|
3040
2642
|
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
3041
|
-
return {
|
|
2643
|
+
return {
|
|
2644
|
+
nodeId,
|
|
2645
|
+
node,
|
|
2646
|
+
result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
|
|
2647
|
+
};
|
|
3042
2648
|
}
|
|
3043
2649
|
logWorkflowNodeRunning(nodeId, options);
|
|
3044
|
-
const result = await
|
|
3045
|
-
return { nodeId, node, result };
|
|
2650
|
+
const result = await nodeExecutor.runNode(nodeId, node, context);
|
|
2651
|
+
return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
|
|
3046
2652
|
}));
|
|
3047
2653
|
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
3048
2654
|
if (firstRejection) {
|
|
@@ -3070,7 +2676,7 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3070
2676
|
}
|
|
3071
2677
|
const lastNodeId = executionOrder[executionOrder.length - 1];
|
|
3072
2678
|
const lastNode = workflowNodes[lastNodeId];
|
|
3073
|
-
const outputKey =
|
|
2679
|
+
const outputKey = workflowOutput ?? lastNode.output ?? lastNodeId;
|
|
3074
2680
|
const result = {
|
|
3075
2681
|
timestamp: new Date().toISOString(),
|
|
3076
2682
|
workflow: workflowName,
|
|
@@ -3092,8 +2698,14 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
3092
2698
|
if (options.jsonOutput) {
|
|
3093
2699
|
console.log(formatWorkflowJson(result));
|
|
3094
2700
|
}
|
|
3095
|
-
else
|
|
3096
|
-
|
|
2701
|
+
else {
|
|
2702
|
+
const wikiSummary = getWikiRunSummary(result.output);
|
|
2703
|
+
if (wikiSummary) {
|
|
2704
|
+
console.log(`\n${formatWikiRunSummaryHuman(wikiSummary)}`);
|
|
2705
|
+
}
|
|
2706
|
+
else if (typeof result.output === 'string' && result.output.trim()) {
|
|
2707
|
+
console.log(`\n${result.output}`);
|
|
2708
|
+
}
|
|
3097
2709
|
}
|
|
3098
2710
|
return result;
|
|
3099
2711
|
}
|
|
@@ -3119,6 +2731,7 @@ export function listWorkflows(config, options = {}) {
|
|
|
3119
2731
|
source: info.source,
|
|
3120
2732
|
overridden: info.overridesPackaged,
|
|
3121
2733
|
description: workflow.description,
|
|
2734
|
+
metadata: workflow.metadata,
|
|
3122
2735
|
};
|
|
3123
2736
|
})
|
|
3124
2737
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3171,11 +2784,13 @@ function buildWorkflowDetail(name, workflow, workingDir) {
|
|
|
3171
2784
|
};
|
|
3172
2785
|
const workflowNodes = getWorkflowNodes(name, workflow);
|
|
3173
2786
|
getWorkflowExecutionOrder(workflowNodes);
|
|
2787
|
+
const graph = buildWorkflowGraph(name, workflow);
|
|
3174
2788
|
return {
|
|
3175
2789
|
name,
|
|
3176
2790
|
source: info.source,
|
|
3177
2791
|
overridden: info.overridesPackaged,
|
|
3178
2792
|
description: workflow.description,
|
|
2793
|
+
metadata: workflow.metadata,
|
|
3179
2794
|
inputs: workflow.inputs ?? {},
|
|
3180
2795
|
output: workflow.output,
|
|
3181
2796
|
nodes: Object.entries(workflowNodes).map(([nodeId, node]) => ({
|
|
@@ -3192,10 +2807,42 @@ function buildWorkflowDetail(name, workflow, workingDir) {
|
|
|
3192
2807
|
output: node.output,
|
|
3193
2808
|
writes: node.writes,
|
|
3194
2809
|
json: node.json,
|
|
2810
|
+
permissions: node.permissions,
|
|
2811
|
+
validation: node.validation,
|
|
3195
2812
|
routes: getWorkflowNodeRoutes(node),
|
|
3196
2813
|
})),
|
|
2814
|
+
graph,
|
|
3197
2815
|
};
|
|
3198
2816
|
}
|
|
2817
|
+
export function showWorkflowGraph(config, workflowName, options = {}) {
|
|
2818
|
+
const workflow = config.workflows?.[workflowName];
|
|
2819
|
+
if (!workflow) {
|
|
2820
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
2821
|
+
}
|
|
2822
|
+
const graph = buildWorkflowGraph(workflowName, workflow);
|
|
2823
|
+
const format = options.format ?? 'text';
|
|
2824
|
+
if (format === 'json') {
|
|
2825
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
2826
|
+
}
|
|
2827
|
+
else if (format === 'mermaid') {
|
|
2828
|
+
console.log(formatWorkflowGraphMermaid(graph));
|
|
2829
|
+
}
|
|
2830
|
+
else {
|
|
2831
|
+
console.log(chalk.bold(`\nWorkflow Graph: ${graph.workflow}\n`));
|
|
2832
|
+
for (const node of graph.nodes) {
|
|
2833
|
+
const outgoing = graph.edges.filter((edge) => edge.source === node.id);
|
|
2834
|
+
const suffix = node.condition ? chalk.yellow(` if ${node.condition}`) : '';
|
|
2835
|
+
console.log(` ${chalk.white(node.id)} ${chalk.gray(`[${node.kind}]`)}${suffix}`);
|
|
2836
|
+
for (const edge of outgoing) {
|
|
2837
|
+
const edgeLabel = edge.label ? ` ${chalk.gray(`(${edge.label})`)}` : '';
|
|
2838
|
+
const marker = edge.kind === 'control' ? chalk.cyan('~>') : chalk.gray('->');
|
|
2839
|
+
console.log(` ${marker} ${edge.target}${edgeLabel}`);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
console.log('');
|
|
2843
|
+
}
|
|
2844
|
+
return graph;
|
|
2845
|
+
}
|
|
3199
2846
|
export function showWorkflow(config, workflowName, options = {}) {
|
|
3200
2847
|
const workflow = config.workflows?.[workflowName];
|
|
3201
2848
|
if (!workflow) {
|
|
@@ -3216,6 +2863,9 @@ export function showWorkflow(config, workflowName, options = {}) {
|
|
|
3216
2863
|
if (detail.output) {
|
|
3217
2864
|
console.log(` Output: ${detail.output}`);
|
|
3218
2865
|
}
|
|
2866
|
+
if (detail.metadata) {
|
|
2867
|
+
console.log(` Metadata: ${JSON.stringify(detail.metadata)}`);
|
|
2868
|
+
}
|
|
3219
2869
|
console.log(chalk.bold('\nInputs:'));
|
|
3220
2870
|
const inputEntries = Object.entries(detail.inputs);
|
|
3221
2871
|
if (inputEntries.length === 0) {
|
|
@@ -3246,6 +2896,10 @@ export function showWorkflow(config, workflowName, options = {}) {
|
|
|
3246
2896
|
console.log(` output: ${node.output}`);
|
|
3247
2897
|
if (node.writes)
|
|
3248
2898
|
console.log(` writes: ${node.writes}`);
|
|
2899
|
+
if (node.permissions)
|
|
2900
|
+
console.log(` permissions: ${JSON.stringify(node.permissions)}`);
|
|
2901
|
+
if (node.validation)
|
|
2902
|
+
console.log(` validation: ${JSON.stringify(node.validation)}`);
|
|
3249
2903
|
if (node.input)
|
|
3250
2904
|
console.log(` input: ${node.input.split('\n')[0]}`);
|
|
3251
2905
|
if (node.with && Object.keys(node.with).length > 0) {
|
|
@@ -3306,4 +2960,15 @@ export function validateWorkflows(config, workflowName, options = {}) {
|
|
|
3306
2960
|
console.log('');
|
|
3307
2961
|
return results;
|
|
3308
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Default in-process {@link WorkflowExecutor}. Delegates to {@link runWorkflow}.
|
|
2965
|
+
*
|
|
2966
|
+
* The Temporal executor will implement the same interface so `drs workflow run`
|
|
2967
|
+
* can dispatch through either backend once `--executor temporal` is added.
|
|
2968
|
+
*/
|
|
2969
|
+
export class LocalWorkflowExecutor {
|
|
2970
|
+
async run(config, workflowName, options = {}) {
|
|
2971
|
+
return runWorkflow(config, workflowName, options);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
3309
2974
|
//# sourceMappingURL=workflow.js.map
|