@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.
Files changed (218) hide show
  1. package/.pi/agents/review/unified-reviewer.md +2 -6
  2. package/.pi/agents/task/okf-wiki-maintainer.md +72 -0
  3. package/.pi/agents/task/review-assistant.md +22 -0
  4. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +12 -4
  5. package/.pi/workflows/github-pr-review-post.yaml +29 -20
  6. package/.pi/workflows/github-pr-review.yaml +37 -8
  7. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +2 -0
  8. package/.pi/workflows/github-pr-visual-explain.yaml +46 -1
  9. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +12 -4
  10. package/.pi/workflows/gitlab-mr-review.yaml +20 -8
  11. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +2 -0
  12. package/.pi/workflows/gitlab-mr-visual-explain.yaml +46 -1
  13. package/.pi/workflows/local-fix-review-issues.yaml +13 -6
  14. package/.pi/workflows/local-review.yaml +7 -1
  15. package/.pi/workflows/local-visual-explain.yaml +46 -1
  16. package/.pi/workflows/release-changelog-finalize.yaml +10 -0
  17. package/.pi/workflows/repository-wiki-check.yaml +40 -0
  18. package/.pi/workflows/repository-wiki-sync.yaml +187 -0
  19. package/.pi/workflows/temporal-control-smoke.yaml +63 -0
  20. package/.wiki-site/.vitepress/config.mts +452 -0
  21. package/.wiki-site/.vitepress/theme/PageLead.vue +122 -0
  22. package/.wiki-site/.vitepress/theme/index.ts +12 -0
  23. package/.wiki-site/.vitepress/theme/style.css +270 -0
  24. package/README.md +77 -4
  25. package/dist/cli/index.js +87 -4
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/init.d.ts +6 -1
  28. package/dist/cli/init.d.ts.map +1 -1
  29. package/dist/cli/init.js +26 -10
  30. package/dist/cli/init.js.map +1 -1
  31. package/dist/cli/run-agent.d.ts +5 -7
  32. package/dist/cli/run-agent.d.ts.map +1 -1
  33. package/dist/cli/run-agent.js +56 -5
  34. package/dist/cli/run-agent.js.map +1 -1
  35. package/dist/cli/wiki.d.ts +3 -0
  36. package/dist/cli/wiki.d.ts.map +1 -0
  37. package/dist/cli/wiki.js +174 -0
  38. package/dist/cli/wiki.js.map +1 -0
  39. package/dist/cli/workflow-executor-selection.d.ts +3 -0
  40. package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
  41. package/dist/cli/workflow-executor-selection.js +15 -0
  42. package/dist/cli/workflow-executor-selection.js.map +1 -0
  43. package/dist/cli/workflow.d.ts +40 -42
  44. package/dist/cli/workflow.d.ts.map +1 -1
  45. package/dist/cli/workflow.js +533 -868
  46. package/dist/cli/workflow.js.map +1 -1
  47. package/dist/github/client.d.ts.map +1 -1
  48. package/dist/github/client.js +2 -2
  49. package/dist/github/client.js.map +1 -1
  50. package/dist/github/platform-adapter.d.ts.map +1 -1
  51. package/dist/github/platform-adapter.js +15 -1
  52. package/dist/github/platform-adapter.js.map +1 -1
  53. package/dist/gitlab/client.d.ts +4 -0
  54. package/dist/gitlab/client.d.ts.map +1 -1
  55. package/dist/gitlab/client.js +17 -1
  56. package/dist/gitlab/client.js.map +1 -1
  57. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  58. package/dist/gitlab/platform-adapter.js +17 -1
  59. package/dist/gitlab/platform-adapter.js.map +1 -1
  60. package/dist/lib/agent-permissions.d.ts +44 -0
  61. package/dist/lib/agent-permissions.d.ts.map +1 -0
  62. package/dist/lib/agent-permissions.js +358 -0
  63. package/dist/lib/agent-permissions.js.map +1 -0
  64. package/dist/lib/agent-result.d.ts +10 -0
  65. package/dist/lib/agent-result.d.ts.map +1 -0
  66. package/dist/lib/agent-result.js +2 -0
  67. package/dist/lib/agent-result.js.map +1 -0
  68. package/dist/lib/comment-poster.d.ts +1 -1
  69. package/dist/lib/comment-poster.d.ts.map +1 -1
  70. package/dist/lib/comment-poster.js +38 -16
  71. package/dist/lib/comment-poster.js.map +1 -1
  72. package/dist/lib/config.d.ts +27 -1
  73. package/dist/lib/config.d.ts.map +1 -1
  74. package/dist/lib/config.js +17 -0
  75. package/dist/lib/config.js.map +1 -1
  76. package/dist/lib/error-comment-poster.d.ts +1 -1
  77. package/dist/lib/error-comment-poster.d.ts.map +1 -1
  78. package/dist/lib/error-comment-poster.js +15 -7
  79. package/dist/lib/error-comment-poster.js.map +1 -1
  80. package/dist/lib/okf-wiki.d.ts +65 -0
  81. package/dist/lib/okf-wiki.d.ts.map +1 -0
  82. package/dist/lib/okf-wiki.js +647 -0
  83. package/dist/lib/okf-wiki.js.map +1 -0
  84. package/dist/lib/output-paths.d.ts +0 -1
  85. package/dist/lib/output-paths.d.ts.map +1 -1
  86. package/dist/lib/output-paths.js +0 -1
  87. package/dist/lib/output-paths.js.map +1 -1
  88. package/dist/lib/platform-client.d.ts +2 -0
  89. package/dist/lib/platform-client.d.ts.map +1 -1
  90. package/dist/lib/project-setup.d.ts +7 -0
  91. package/dist/lib/project-setup.d.ts.map +1 -0
  92. package/dist/lib/project-setup.js +11 -0
  93. package/dist/lib/project-setup.js.map +1 -0
  94. package/dist/lib/review-artifact-store.d.ts +16 -0
  95. package/dist/lib/review-artifact-store.d.ts.map +1 -0
  96. package/dist/lib/review-artifact-store.js +84 -0
  97. package/dist/lib/review-artifact-store.js.map +1 -0
  98. package/dist/lib/review-artifact.d.ts +11 -1
  99. package/dist/lib/review-artifact.d.ts.map +1 -1
  100. package/dist/lib/review-artifact.js +203 -0
  101. package/dist/lib/review-artifact.js.map +1 -1
  102. package/dist/lib/review-core.d.ts +1 -0
  103. package/dist/lib/review-core.d.ts.map +1 -1
  104. package/dist/lib/review-core.js +11 -14
  105. package/dist/lib/review-core.js.map +1 -1
  106. package/dist/lib/review-orchestrator.d.ts +6 -1
  107. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  108. package/dist/lib/review-orchestrator.js +3 -1
  109. package/dist/lib/review-orchestrator.js.map +1 -1
  110. package/dist/lib/review-parser.d.ts.map +1 -1
  111. package/dist/lib/review-parser.js +15 -12
  112. package/dist/lib/review-parser.js.map +1 -1
  113. package/dist/lib/wiki-delta.d.ts +71 -0
  114. package/dist/lib/wiki-delta.d.ts.map +1 -0
  115. package/dist/lib/wiki-delta.js +524 -0
  116. package/dist/lib/wiki-delta.js.map +1 -0
  117. package/dist/lib/wiki-run-summary.d.ts +45 -0
  118. package/dist/lib/wiki-run-summary.d.ts.map +1 -0
  119. package/dist/lib/wiki-run-summary.js +168 -0
  120. package/dist/lib/wiki-run-summary.js.map +1 -0
  121. package/dist/lib/wiki-search.d.ts +24 -0
  122. package/dist/lib/wiki-search.d.ts.map +1 -0
  123. package/dist/lib/wiki-search.js +200 -0
  124. package/dist/lib/wiki-search.js.map +1 -0
  125. package/dist/lib/wiki-site-graph.d.ts +33 -0
  126. package/dist/lib/wiki-site-graph.d.ts.map +1 -0
  127. package/dist/lib/wiki-site-graph.js +446 -0
  128. package/dist/lib/wiki-site-graph.js.map +1 -0
  129. package/dist/lib/wiki-site-safety.d.ts +6 -0
  130. package/dist/lib/wiki-site-safety.d.ts.map +1 -0
  131. package/dist/lib/wiki-site-safety.js +83 -0
  132. package/dist/lib/wiki-site-safety.js.map +1 -0
  133. package/dist/lib/wiki-site-smoke.d.ts +16 -0
  134. package/dist/lib/wiki-site-smoke.d.ts.map +1 -0
  135. package/dist/lib/wiki-site-smoke.js +270 -0
  136. package/dist/lib/wiki-site-smoke.js.map +1 -0
  137. package/dist/lib/wiki-site.d.ts +26 -0
  138. package/dist/lib/wiki-site.d.ts.map +1 -0
  139. package/dist/lib/wiki-site.js +221 -0
  140. package/dist/lib/wiki-site.js.map +1 -0
  141. package/dist/lib/workflow/artifact-store.d.ts +71 -0
  142. package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
  143. package/dist/lib/workflow/artifact-store.js +121 -0
  144. package/dist/lib/workflow/artifact-store.js.map +1 -0
  145. package/dist/lib/workflow/compiled-plan.d.ts +87 -0
  146. package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
  147. package/dist/lib/workflow/compiled-plan.js +62 -0
  148. package/dist/lib/workflow/compiled-plan.js.map +1 -0
  149. package/dist/lib/workflow/executor.d.ts +18 -0
  150. package/dist/lib/workflow/executor.d.ts.map +1 -0
  151. package/dist/lib/workflow/executor.js +2 -0
  152. package/dist/lib/workflow/executor.js.map +1 -0
  153. package/dist/lib/workflow/graph.d.ts +30 -0
  154. package/dist/lib/workflow/graph.d.ts.map +1 -0
  155. package/dist/lib/workflow/graph.js +139 -0
  156. package/dist/lib/workflow/graph.js.map +1 -0
  157. package/dist/lib/workflow/input.d.ts +4 -0
  158. package/dist/lib/workflow/input.d.ts.map +1 -0
  159. package/dist/lib/workflow/input.js +4 -0
  160. package/dist/lib/workflow/input.js.map +1 -0
  161. package/dist/lib/workflow/node-executor.d.ts +19 -0
  162. package/dist/lib/workflow/node-executor.d.ts.map +1 -0
  163. package/dist/lib/workflow/node-executor.js +2 -0
  164. package/dist/lib/workflow/node-executor.js.map +1 -0
  165. package/dist/lib/workflow/planning.d.ts +52 -0
  166. package/dist/lib/workflow/planning.d.ts.map +1 -0
  167. package/dist/lib/workflow/planning.js +865 -0
  168. package/dist/lib/workflow/planning.js.map +1 -0
  169. package/dist/lib/workflow/types.d.ts +71 -0
  170. package/dist/lib/workflow/types.d.ts.map +1 -0
  171. package/dist/lib/workflow/types.js +2 -0
  172. package/dist/lib/workflow/types.js.map +1 -0
  173. package/dist/lib/workflow-artifacts.d.ts.map +1 -1
  174. package/dist/lib/workflow-artifacts.js +23 -1
  175. package/dist/lib/workflow-artifacts.js.map +1 -1
  176. package/dist/lib/write-json-output.d.ts.map +1 -1
  177. package/dist/lib/write-json-output.js +3 -8
  178. package/dist/lib/write-json-output.js.map +1 -1
  179. package/dist/pi/sdk.d.ts.map +1 -1
  180. package/dist/pi/sdk.js +276 -19
  181. package/dist/pi/sdk.js.map +1 -1
  182. package/dist/runtime/client.d.ts +3 -0
  183. package/dist/runtime/client.d.ts.map +1 -1
  184. package/dist/runtime/client.js +4 -0
  185. package/dist/runtime/client.js.map +1 -1
  186. package/dist/temporal/activities.d.ts +26 -0
  187. package/dist/temporal/activities.d.ts.map +1 -0
  188. package/dist/temporal/activities.js +219 -0
  189. package/dist/temporal/activities.js.map +1 -0
  190. package/dist/temporal/config.d.ts +4 -0
  191. package/dist/temporal/config.d.ts.map +1 -0
  192. package/dist/temporal/config.js +36 -0
  193. package/dist/temporal/config.js.map +1 -0
  194. package/dist/temporal/executor.d.ts +7 -0
  195. package/dist/temporal/executor.d.ts.map +1 -0
  196. package/dist/temporal/executor.js +240 -0
  197. package/dist/temporal/executor.js.map +1 -0
  198. package/dist/temporal/retry-policy.d.ts +28 -0
  199. package/dist/temporal/retry-policy.d.ts.map +1 -0
  200. package/dist/temporal/retry-policy.js +61 -0
  201. package/dist/temporal/retry-policy.js.map +1 -0
  202. package/dist/temporal/types.d.ts +86 -0
  203. package/dist/temporal/types.d.ts.map +1 -0
  204. package/dist/temporal/types.js +2 -0
  205. package/dist/temporal/types.js.map +1 -0
  206. package/dist/temporal/worker.d.ts +3 -0
  207. package/dist/temporal/worker.d.ts.map +1 -0
  208. package/dist/temporal/worker.js +18 -0
  209. package/dist/temporal/worker.js.map +1 -0
  210. package/dist/temporal/workflow-id.d.ts +27 -0
  211. package/dist/temporal/workflow-id.d.ts.map +1 -0
  212. package/dist/temporal/workflow-id.js +53 -0
  213. package/dist/temporal/workflow-id.js.map +1 -0
  214. package/dist/temporal/workflows.d.ts +6 -0
  215. package/dist/temporal/workflows.d.ts.map +1 -0
  216. package/dist/temporal/workflows.js +355 -0
  217. package/dist/temporal/workflows.js.map +1 -0
  218. package/package.json +24 -9
@@ -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 git.commit(message, paths) : await git.commit(message);
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 [pullRequest, changedFiles] = await Promise.all([
1565
- platformClient.getPullRequest(projectId, prNumber),
1566
- platformClient.getChangedFiles(projectId, prNumber),
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 marker = getStringActionOption(node, 'marker', context)?.trim();
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 reviewResult = context.artifacts[reviewArtifact];
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
- await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
2096
- if (shouldRemoveErrorComment) {
2097
- await removeErrorComment(target.platformClient, target.projectId, target.prNumber);
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
- return await executeReview(config, {
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 artifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
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
- let artifactResponse = '';
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
- async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeIds, options, workingDir, context, executionContext) {
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 { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
2419
+ return {
2420
+ nodeId,
2421
+ node,
2422
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2423
+ };
2807
2424
  }
2808
2425
  logWorkflowNodeRunning(nodeId, options);
2809
- const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
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 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}".`);
2442
+ async function runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor) {
2443
+ await runControlWorkflowFromSegments(workflowNodes, splitWorkflowSegments(workflowNodes, executionOrder), options, context, nodeExecutor);
2935
2444
  }
2936
- async function runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext) {
2937
- const segments = splitWorkflowSegments(workflowNodes, executionOrder);
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(config, workflowNodes, segment.nodeIds, segment.activeNodeIds, options, workingDir, context, executionContext);
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 { result, nextNodeId, ended } = skipReason
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
- if (kind === 'agent') {
2991
- return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2992
- }
2993
- if (kind === 'agents') {
2994
- return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
2995
- }
2996
- if (kind === 'control') {
2997
- throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
2998
- }
2999
- return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
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 executionOrder = getWorkflowExecutionOrder(workflowNodes);
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 (hasWorkflowControlNodes(workflowNodes)) {
3029
- await runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext);
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 { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
2643
+ return {
2644
+ nodeId,
2645
+ node,
2646
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2647
+ };
3042
2648
  }
3043
2649
  logWorkflowNodeRunning(nodeId, options);
3044
- const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
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 = workflow.output ?? lastNode.output ?? lastNodeId;
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 if (typeof result.output === 'string' && result.output.trim()) {
3096
- console.log(`\n${result.output}`);
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