@diff-review-system/drs 4.0.0-rc.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/.pi/agents/describe/pr-describer.md +14 -0
  2. package/.pi/agents/review/unified-reviewer.md +33 -7
  3. package/.pi/agents/task/agents-md-updater.md +3 -1
  4. package/.pi/agents/task/review-assistant.md +22 -0
  5. package/.pi/agents/task/review-issue-fixer.md +18 -1
  6. package/.pi/agents/visual/pr-explainer.md +205 -0
  7. package/.pi/workflows/github-pr-describe.yaml +10 -7
  8. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +154 -0
  9. package/.pi/workflows/github-pr-post-comment.yaml +10 -10
  10. package/.pi/workflows/github-pr-review-post.yaml +18 -8
  11. package/.pi/workflows/github-pr-review.yaml +354 -7
  12. package/.pi/workflows/github-pr-show-changes.yaml +8 -8
  13. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +103 -0
  14. package/.pi/workflows/github-pr-visual-explain.yaml +80 -0
  15. package/.pi/workflows/gitlab-mr-describe.yaml +8 -5
  16. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +150 -0
  17. package/.pi/workflows/gitlab-mr-post-comment.yaml +8 -8
  18. package/.pi/workflows/gitlab-mr-review.yaml +354 -5
  19. package/.pi/workflows/gitlab-mr-show-changes.yaml +6 -6
  20. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +100 -0
  21. package/.pi/workflows/gitlab-mr-visual-explain.yaml +78 -0
  22. package/.pi/workflows/local-fix-review-issues.yaml +89 -13
  23. package/.pi/workflows/local-review.yaml +15 -2
  24. package/.pi/workflows/local-update-agents-md.yaml +1 -1
  25. package/.pi/workflows/local-visual-explain.yaml +76 -0
  26. package/.pi/workflows/release-changelog-finalize.yaml +47 -0
  27. package/.pi/workflows/tag-changelog-update.yaml +4 -4
  28. package/.pi/workflows/temporal-control-smoke.yaml +63 -0
  29. package/README.md +130 -27
  30. package/dist/ci/runner.d.ts.map +1 -1
  31. package/dist/ci/runner.js +3 -1
  32. package/dist/ci/runner.js.map +1 -1
  33. package/dist/cli/index.js +258 -9
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/cli/init.d.ts +6 -1
  36. package/dist/cli/init.d.ts.map +1 -1
  37. package/dist/cli/init.js +29 -10
  38. package/dist/cli/init.js.map +1 -1
  39. package/dist/cli/run-agent.d.ts +4 -7
  40. package/dist/cli/run-agent.d.ts.map +1 -1
  41. package/dist/cli/run-agent.js +4 -0
  42. package/dist/cli/run-agent.js.map +1 -1
  43. package/dist/cli/workflow-executor-selection.d.ts +3 -0
  44. package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
  45. package/dist/cli/workflow-executor-selection.js +15 -0
  46. package/dist/cli/workflow-executor-selection.js.map +1 -0
  47. package/dist/cli/workflow.d.ts +76 -26
  48. package/dist/cli/workflow.d.ts.map +1 -1
  49. package/dist/cli/workflow.js +1624 -192
  50. package/dist/cli/workflow.js.map +1 -1
  51. package/dist/github/client.d.ts +12 -0
  52. package/dist/github/client.d.ts.map +1 -1
  53. package/dist/github/client.js +27 -0
  54. package/dist/github/client.js.map +1 -1
  55. package/dist/github/platform-adapter.d.ts +6 -1
  56. package/dist/github/platform-adapter.d.ts.map +1 -1
  57. package/dist/github/platform-adapter.js +84 -8
  58. package/dist/github/platform-adapter.js.map +1 -1
  59. package/dist/gitlab/client.d.ts +11 -0
  60. package/dist/gitlab/client.d.ts.map +1 -1
  61. package/dist/gitlab/client.js +11 -0
  62. package/dist/gitlab/client.js.map +1 -1
  63. package/dist/gitlab/platform-adapter.d.ts +3 -1
  64. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  65. package/dist/gitlab/platform-adapter.js +32 -1
  66. package/dist/gitlab/platform-adapter.js.map +1 -1
  67. package/dist/lib/agent-result.d.ts +8 -0
  68. package/dist/lib/agent-result.d.ts.map +1 -0
  69. package/dist/lib/agent-result.js +2 -0
  70. package/dist/lib/agent-result.js.map +1 -0
  71. package/dist/lib/comment-formatter.d.ts +8 -0
  72. package/dist/lib/comment-formatter.d.ts.map +1 -1
  73. package/dist/lib/comment-formatter.js +12 -4
  74. package/dist/lib/comment-formatter.js.map +1 -1
  75. package/dist/lib/comment-poster.d.ts.map +1 -1
  76. package/dist/lib/comment-poster.js +28 -1
  77. package/dist/lib/comment-poster.js.map +1 -1
  78. package/dist/lib/config.d.ts +71 -11
  79. package/dist/lib/config.d.ts.map +1 -1
  80. package/dist/lib/config.js +163 -28
  81. package/dist/lib/config.js.map +1 -1
  82. package/dist/lib/context-compression.d.ts +10 -0
  83. package/dist/lib/context-compression.d.ts.map +1 -1
  84. package/dist/lib/context-compression.js +101 -13
  85. package/dist/lib/context-compression.js.map +1 -1
  86. package/dist/lib/context-loader.d.ts +2 -1
  87. package/dist/lib/context-loader.d.ts.map +1 -1
  88. package/dist/lib/context-loader.js +70 -1
  89. package/dist/lib/context-loader.js.map +1 -1
  90. package/dist/lib/describe-core.d.ts.map +1 -1
  91. package/dist/lib/describe-core.js +3 -2
  92. package/dist/lib/describe-core.js.map +1 -1
  93. package/dist/lib/diff-lines.d.ts +9 -0
  94. package/dist/lib/diff-lines.d.ts.map +1 -1
  95. package/dist/lib/diff-lines.js +17 -9
  96. package/dist/lib/diff-lines.js.map +1 -1
  97. package/dist/lib/exit.js +4 -4
  98. package/dist/lib/exit.js.map +1 -1
  99. package/dist/lib/html-artifact.d.ts +14 -0
  100. package/dist/lib/html-artifact.d.ts.map +1 -0
  101. package/dist/lib/html-artifact.js +59 -0
  102. package/dist/lib/html-artifact.js.map +1 -0
  103. package/dist/lib/issue-parser.js +3 -3
  104. package/dist/lib/issue-parser.js.map +1 -1
  105. package/dist/lib/json-output-schema.d.ts +70 -0
  106. package/dist/lib/json-output-schema.d.ts.map +1 -1
  107. package/dist/lib/json-output-schema.js +40 -0
  108. package/dist/lib/json-output-schema.js.map +1 -1
  109. package/dist/lib/output-paths.d.ts +0 -1
  110. package/dist/lib/output-paths.d.ts.map +1 -1
  111. package/dist/lib/output-paths.js +0 -1
  112. package/dist/lib/output-paths.js.map +1 -1
  113. package/dist/lib/platform-client.d.ts +26 -0
  114. package/dist/lib/platform-client.d.ts.map +1 -1
  115. package/dist/lib/project-setup.d.ts +10 -0
  116. package/dist/lib/project-setup.d.ts.map +1 -0
  117. package/dist/lib/project-setup.js +25 -0
  118. package/dist/lib/project-setup.js.map +1 -0
  119. package/dist/lib/review-artifact-store.d.ts +16 -0
  120. package/dist/lib/review-artifact-store.d.ts.map +1 -0
  121. package/dist/lib/review-artifact-store.js +84 -0
  122. package/dist/lib/review-artifact-store.js.map +1 -0
  123. package/dist/lib/review-artifact.d.ts +69 -0
  124. package/dist/lib/review-artifact.d.ts.map +1 -0
  125. package/dist/lib/review-artifact.js +172 -0
  126. package/dist/lib/review-artifact.js.map +1 -0
  127. package/dist/lib/review-core.d.ts +7 -4
  128. package/dist/lib/review-core.d.ts.map +1 -1
  129. package/dist/lib/review-core.js +82 -165
  130. package/dist/lib/review-core.js.map +1 -1
  131. package/dist/lib/review-orchestrator.d.ts +23 -0
  132. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  133. package/dist/lib/review-orchestrator.js +20 -13
  134. package/dist/lib/review-orchestrator.js.map +1 -1
  135. package/dist/lib/review-parser.d.ts.map +1 -1
  136. package/dist/lib/review-parser.js +15 -12
  137. package/dist/lib/review-parser.js.map +1 -1
  138. package/dist/lib/review-usage.d.ts +4 -0
  139. package/dist/lib/review-usage.d.ts.map +1 -1
  140. package/dist/lib/review-usage.js +25 -0
  141. package/dist/lib/review-usage.js.map +1 -1
  142. package/dist/lib/skills.d.ts +16 -0
  143. package/dist/lib/skills.d.ts.map +1 -0
  144. package/dist/lib/skills.js +140 -0
  145. package/dist/lib/skills.js.map +1 -0
  146. package/dist/lib/trace-collector.d.ts +105 -0
  147. package/dist/lib/trace-collector.d.ts.map +1 -0
  148. package/dist/lib/trace-collector.js +255 -0
  149. package/dist/lib/trace-collector.js.map +1 -0
  150. package/dist/lib/trace-html.d.ts +3 -0
  151. package/dist/lib/trace-html.d.ts.map +1 -0
  152. package/dist/lib/trace-html.js +349 -0
  153. package/dist/lib/trace-html.js.map +1 -0
  154. package/dist/lib/workflow/artifact-store.d.ts +71 -0
  155. package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
  156. package/dist/lib/workflow/artifact-store.js +121 -0
  157. package/dist/lib/workflow/artifact-store.js.map +1 -0
  158. package/dist/lib/workflow/compiled-plan.d.ts +87 -0
  159. package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
  160. package/dist/lib/workflow/compiled-plan.js +62 -0
  161. package/dist/lib/workflow/compiled-plan.js.map +1 -0
  162. package/dist/lib/workflow/executor.d.ts +18 -0
  163. package/dist/lib/workflow/executor.d.ts.map +1 -0
  164. package/dist/lib/workflow/executor.js +2 -0
  165. package/dist/lib/workflow/executor.js.map +1 -0
  166. package/dist/lib/workflow/graph.d.ts +30 -0
  167. package/dist/lib/workflow/graph.d.ts.map +1 -0
  168. package/dist/lib/workflow/graph.js +139 -0
  169. package/dist/lib/workflow/graph.js.map +1 -0
  170. package/dist/lib/workflow/input.d.ts +4 -0
  171. package/dist/lib/workflow/input.d.ts.map +1 -0
  172. package/dist/lib/workflow/input.js +4 -0
  173. package/dist/lib/workflow/input.js.map +1 -0
  174. package/dist/lib/workflow/node-executor.d.ts +19 -0
  175. package/dist/lib/workflow/node-executor.d.ts.map +1 -0
  176. package/dist/lib/workflow/node-executor.js +2 -0
  177. package/dist/lib/workflow/node-executor.js.map +1 -0
  178. package/dist/lib/workflow/planning.d.ts +51 -0
  179. package/dist/lib/workflow/planning.d.ts.map +1 -0
  180. package/dist/lib/workflow/planning.js +814 -0
  181. package/dist/lib/workflow/planning.js.map +1 -0
  182. package/dist/lib/workflow/types.d.ts +66 -0
  183. package/dist/lib/workflow/types.d.ts.map +1 -0
  184. package/dist/lib/workflow/types.js +2 -0
  185. package/dist/lib/workflow/types.js.map +1 -0
  186. package/dist/lib/workflow-artifacts.d.ts +54 -0
  187. package/dist/lib/workflow-artifacts.d.ts.map +1 -0
  188. package/dist/lib/workflow-artifacts.js +150 -0
  189. package/dist/lib/workflow-artifacts.js.map +1 -0
  190. package/dist/lib/write-json-output.d.ts.map +1 -1
  191. package/dist/lib/write-json-output.js +3 -8
  192. package/dist/lib/write-json-output.js.map +1 -1
  193. package/dist/pi/sdk.d.ts.map +1 -1
  194. package/dist/pi/sdk.js +576 -8
  195. package/dist/pi/sdk.js.map +1 -1
  196. package/dist/runtime/agent-loader.js +2 -2
  197. package/dist/runtime/built-in-paths.d.ts +1 -0
  198. package/dist/runtime/built-in-paths.d.ts.map +1 -1
  199. package/dist/runtime/built-in-paths.js +7 -0
  200. package/dist/runtime/built-in-paths.js.map +1 -1
  201. package/dist/runtime/client.d.ts +2 -0
  202. package/dist/runtime/client.d.ts.map +1 -1
  203. package/dist/runtime/client.js +11 -5
  204. package/dist/runtime/client.js.map +1 -1
  205. package/dist/temporal/activities.d.ts +26 -0
  206. package/dist/temporal/activities.d.ts.map +1 -0
  207. package/dist/temporal/activities.js +219 -0
  208. package/dist/temporal/activities.js.map +1 -0
  209. package/dist/temporal/config.d.ts +4 -0
  210. package/dist/temporal/config.d.ts.map +1 -0
  211. package/dist/temporal/config.js +36 -0
  212. package/dist/temporal/config.js.map +1 -0
  213. package/dist/temporal/executor.d.ts +7 -0
  214. package/dist/temporal/executor.d.ts.map +1 -0
  215. package/dist/temporal/executor.js +233 -0
  216. package/dist/temporal/executor.js.map +1 -0
  217. package/dist/temporal/retry-policy.d.ts +28 -0
  218. package/dist/temporal/retry-policy.d.ts.map +1 -0
  219. package/dist/temporal/retry-policy.js +56 -0
  220. package/dist/temporal/retry-policy.js.map +1 -0
  221. package/dist/temporal/types.d.ts +86 -0
  222. package/dist/temporal/types.d.ts.map +1 -0
  223. package/dist/temporal/types.js +2 -0
  224. package/dist/temporal/types.js.map +1 -0
  225. package/dist/temporal/worker.d.ts +3 -0
  226. package/dist/temporal/worker.d.ts.map +1 -0
  227. package/dist/temporal/worker.js +18 -0
  228. package/dist/temporal/worker.js.map +1 -0
  229. package/dist/temporal/workflow-id.d.ts +27 -0
  230. package/dist/temporal/workflow-id.d.ts.map +1 -0
  231. package/dist/temporal/workflow-id.js +53 -0
  232. package/dist/temporal/workflow-id.js.map +1 -0
  233. package/dist/temporal/workflows.d.ts +6 -0
  234. package/dist/temporal/workflows.d.ts.map +1 -0
  235. package/dist/temporal/workflows.js +342 -0
  236. package/dist/temporal/workflows.js.map +1 -0
  237. package/package.json +26 -15
  238. package/.pi/workflows/github-pr-describe-post.yaml +0 -24
  239. package/.pi/workflows/gitlab-mr-describe-post.yaml +0 -22
  240. package/.pi/workflows/gitlab-mr-review-code-quality.yaml +0 -31
  241. package/.pi/workflows/gitlab-mr-review-post-code-quality.yaml +0 -40
  242. package/.pi/workflows/gitlab-mr-review-post.yaml +0 -30
  243. package/.pi/workflows/local-staged-review.yaml +0 -17
  244. package/dist/cli/run-agent.test.d.ts +0 -2
  245. package/dist/cli/run-agent.test.d.ts.map +0 -1
  246. package/dist/cli/run-agent.test.js +0 -204
  247. package/dist/cli/run-agent.test.js.map +0 -1
  248. package/dist/cli/workflow.test.d.ts +0 -2
  249. package/dist/cli/workflow.test.d.ts.map +0 -1
  250. package/dist/cli/workflow.test.js +0 -1410
  251. package/dist/cli/workflow.test.js.map +0 -1
  252. package/dist/github/client.test.d.ts +0 -2
  253. package/dist/github/client.test.d.ts.map +0 -1
  254. package/dist/github/client.test.js +0 -206
  255. package/dist/github/client.test.js.map +0 -1
  256. package/dist/github/platform-adapter.test.d.ts +0 -2
  257. package/dist/github/platform-adapter.test.d.ts.map +0 -1
  258. package/dist/github/platform-adapter.test.js +0 -40
  259. package/dist/github/platform-adapter.test.js.map +0 -1
  260. package/dist/gitlab/diff-parser.test.d.ts +0 -2
  261. package/dist/gitlab/diff-parser.test.d.ts.map +0 -1
  262. package/dist/gitlab/diff-parser.test.js +0 -315
  263. package/dist/gitlab/diff-parser.test.js.map +0 -1
  264. package/dist/gitlab/platform-adapter.test.d.ts +0 -2
  265. package/dist/gitlab/platform-adapter.test.d.ts.map +0 -1
  266. package/dist/gitlab/platform-adapter.test.js +0 -21
  267. package/dist/gitlab/platform-adapter.test.js.map +0 -1
  268. package/dist/index.test.d.ts +0 -2
  269. package/dist/index.test.d.ts.map +0 -1
  270. package/dist/index.test.js +0 -7
  271. package/dist/index.test.js.map +0 -1
  272. package/dist/lib/code-quality-report.test.d.ts +0 -2
  273. package/dist/lib/code-quality-report.test.d.ts.map +0 -1
  274. package/dist/lib/code-quality-report.test.js +0 -327
  275. package/dist/lib/code-quality-report.test.js.map +0 -1
  276. package/dist/lib/comment-formatter.test.d.ts +0 -2
  277. package/dist/lib/comment-formatter.test.d.ts.map +0 -1
  278. package/dist/lib/comment-formatter.test.js +0 -727
  279. package/dist/lib/comment-formatter.test.js.map +0 -1
  280. package/dist/lib/comment-manager.test.d.ts +0 -2
  281. package/dist/lib/comment-manager.test.d.ts.map +0 -1
  282. package/dist/lib/comment-manager.test.js +0 -680
  283. package/dist/lib/comment-manager.test.js.map +0 -1
  284. package/dist/lib/comment-poster.test.d.ts +0 -5
  285. package/dist/lib/comment-poster.test.d.ts.map +0 -1
  286. package/dist/lib/comment-poster.test.js +0 -255
  287. package/dist/lib/comment-poster.test.js.map +0 -1
  288. package/dist/lib/config-model-overrides.test.d.ts +0 -2
  289. package/dist/lib/config-model-overrides.test.d.ts.map +0 -1
  290. package/dist/lib/config-model-overrides.test.js +0 -218
  291. package/dist/lib/config-model-overrides.test.js.map +0 -1
  292. package/dist/lib/config.test.d.ts +0 -2
  293. package/dist/lib/config.test.d.ts.map +0 -1
  294. package/dist/lib/config.test.js +0 -353
  295. package/dist/lib/config.test.js.map +0 -1
  296. package/dist/lib/context-compression.test.d.ts +0 -2
  297. package/dist/lib/context-compression.test.d.ts.map +0 -1
  298. package/dist/lib/context-compression.test.js +0 -337
  299. package/dist/lib/context-compression.test.js.map +0 -1
  300. package/dist/lib/context-loader.test.d.ts +0 -2
  301. package/dist/lib/context-loader.test.d.ts.map +0 -1
  302. package/dist/lib/context-loader.test.js +0 -212
  303. package/dist/lib/context-loader.test.js.map +0 -1
  304. package/dist/lib/cursor-fix-link.test.d.ts +0 -2
  305. package/dist/lib/cursor-fix-link.test.d.ts.map +0 -1
  306. package/dist/lib/cursor-fix-link.test.js +0 -70
  307. package/dist/lib/cursor-fix-link.test.js.map +0 -1
  308. package/dist/lib/describe-core.test.d.ts +0 -2
  309. package/dist/lib/describe-core.test.d.ts.map +0 -1
  310. package/dist/lib/describe-core.test.js +0 -208
  311. package/dist/lib/describe-core.test.js.map +0 -1
  312. package/dist/lib/describe-output-path.test.d.ts +0 -2
  313. package/dist/lib/describe-output-path.test.d.ts.map +0 -1
  314. package/dist/lib/describe-output-path.test.js +0 -51
  315. package/dist/lib/describe-output-path.test.js.map +0 -1
  316. package/dist/lib/describe-parser.test.d.ts +0 -2
  317. package/dist/lib/describe-parser.test.d.ts.map +0 -1
  318. package/dist/lib/describe-parser.test.js +0 -282
  319. package/dist/lib/describe-parser.test.js.map +0 -1
  320. package/dist/lib/description-executor.test.d.ts +0 -2
  321. package/dist/lib/description-executor.test.d.ts.map +0 -1
  322. package/dist/lib/description-executor.test.js +0 -135
  323. package/dist/lib/description-executor.test.js.map +0 -1
  324. package/dist/lib/description-formatter.test.d.ts +0 -2
  325. package/dist/lib/description-formatter.test.d.ts.map +0 -1
  326. package/dist/lib/description-formatter.test.js +0 -57
  327. package/dist/lib/description-formatter.test.js.map +0 -1
  328. package/dist/lib/diff-lines.test.d.ts +0 -2
  329. package/dist/lib/diff-lines.test.d.ts.map +0 -1
  330. package/dist/lib/diff-lines.test.js +0 -13
  331. package/dist/lib/diff-lines.test.js.map +0 -1
  332. package/dist/lib/diff-parser.test.d.ts +0 -2
  333. package/dist/lib/diff-parser.test.d.ts.map +0 -1
  334. package/dist/lib/diff-parser.test.js +0 -335
  335. package/dist/lib/diff-parser.test.js.map +0 -1
  336. package/dist/lib/error-comment-poster.test.d.ts +0 -2
  337. package/dist/lib/error-comment-poster.test.d.ts.map +0 -1
  338. package/dist/lib/error-comment-poster.test.js +0 -128
  339. package/dist/lib/error-comment-poster.test.js.map +0 -1
  340. package/dist/lib/exit.test.d.ts +0 -2
  341. package/dist/lib/exit.test.d.ts.map +0 -1
  342. package/dist/lib/exit.test.js +0 -120
  343. package/dist/lib/exit.test.js.map +0 -1
  344. package/dist/lib/issue-parser.test.d.ts +0 -2
  345. package/dist/lib/issue-parser.test.d.ts.map +0 -1
  346. package/dist/lib/issue-parser.test.js +0 -281
  347. package/dist/lib/issue-parser.test.js.map +0 -1
  348. package/dist/lib/json-output-schema.test.d.ts +0 -2
  349. package/dist/lib/json-output-schema.test.d.ts.map +0 -1
  350. package/dist/lib/json-output-schema.test.js +0 -92
  351. package/dist/lib/json-output-schema.test.js.map +0 -1
  352. package/dist/lib/json-output.test.d.ts +0 -2
  353. package/dist/lib/json-output.test.d.ts.map +0 -1
  354. package/dist/lib/json-output.test.js +0 -141
  355. package/dist/lib/json-output.test.js.map +0 -1
  356. package/dist/lib/logger.test.d.ts +0 -2
  357. package/dist/lib/logger.test.d.ts.map +0 -1
  358. package/dist/lib/logger.test.js +0 -324
  359. package/dist/lib/logger.test.js.map +0 -1
  360. package/dist/lib/position-validator.test.d.ts +0 -2
  361. package/dist/lib/position-validator.test.d.ts.map +0 -1
  362. package/dist/lib/position-validator.test.js +0 -128
  363. package/dist/lib/position-validator.test.js.map +0 -1
  364. package/dist/lib/prompt-budget.test.d.ts +0 -2
  365. package/dist/lib/prompt-budget.test.d.ts.map +0 -1
  366. package/dist/lib/prompt-budget.test.js +0 -55
  367. package/dist/lib/prompt-budget.test.js.map +0 -1
  368. package/dist/lib/repository-validator.test.d.ts +0 -5
  369. package/dist/lib/repository-validator.test.d.ts.map +0 -1
  370. package/dist/lib/repository-validator.test.js +0 -341
  371. package/dist/lib/repository-validator.test.js.map +0 -1
  372. package/dist/lib/review-core.test.d.ts +0 -2
  373. package/dist/lib/review-core.test.d.ts.map +0 -1
  374. package/dist/lib/review-core.test.js +0 -614
  375. package/dist/lib/review-core.test.js.map +0 -1
  376. package/dist/lib/review-orchestrator.test.d.ts +0 -2
  377. package/dist/lib/review-orchestrator.test.d.ts.map +0 -1
  378. package/dist/lib/review-orchestrator.test.js +0 -552
  379. package/dist/lib/review-orchestrator.test.js.map +0 -1
  380. package/dist/lib/review-output-path.test.d.ts +0 -2
  381. package/dist/lib/review-output-path.test.d.ts.map +0 -1
  382. package/dist/lib/review-output-path.test.js +0 -83
  383. package/dist/lib/review-output-path.test.js.map +0 -1
  384. package/dist/lib/review-parser.test.d.ts +0 -2
  385. package/dist/lib/review-parser.test.d.ts.map +0 -1
  386. package/dist/lib/review-parser.test.js +0 -130
  387. package/dist/lib/review-parser.test.js.map +0 -1
  388. package/dist/lib/review-usage.test.d.ts +0 -2
  389. package/dist/lib/review-usage.test.d.ts.map +0 -1
  390. package/dist/lib/review-usage.test.js +0 -83
  391. package/dist/lib/review-usage.test.js.map +0 -1
  392. package/dist/lib/unified-review-executor.d.ts +0 -58
  393. package/dist/lib/unified-review-executor.d.ts.map +0 -1
  394. package/dist/lib/unified-review-executor.js +0 -201
  395. package/dist/lib/unified-review-executor.js.map +0 -1
  396. package/dist/lib/unified-review-executor.test.d.ts +0 -5
  397. package/dist/lib/unified-review-executor.test.d.ts.map +0 -1
  398. package/dist/lib/unified-review-executor.test.js +0 -472
  399. package/dist/lib/unified-review-executor.test.js.map +0 -1
  400. package/dist/lib/write-json-output.test.d.ts +0 -2
  401. package/dist/lib/write-json-output.test.d.ts.map +0 -1
  402. package/dist/lib/write-json-output.test.js +0 -259
  403. package/dist/lib/write-json-output.test.js.map +0 -1
  404. package/dist/pi/sdk.test.d.ts +0 -2
  405. package/dist/pi/sdk.test.d.ts.map +0 -1
  406. package/dist/pi/sdk.test.js +0 -488
  407. package/dist/pi/sdk.test.js.map +0 -1
  408. package/dist/runtime/agent-loader.test.d.ts +0 -2
  409. package/dist/runtime/agent-loader.test.d.ts.map +0 -1
  410. package/dist/runtime/agent-loader.test.js +0 -277
  411. package/dist/runtime/agent-loader.test.js.map +0 -1
  412. package/dist/runtime/client.test.d.ts +0 -2
  413. package/dist/runtime/client.test.d.ts.map +0 -1
  414. package/dist/runtime/client.test.js +0 -772
  415. package/dist/runtime/client.test.js.map +0 -1
  416. package/dist/runtime/path-config.test.d.ts +0 -2
  417. package/dist/runtime/path-config.test.d.ts.map +0 -1
  418. package/dist/runtime/path-config.test.js +0 -112
  419. package/dist/runtime/path-config.test.js.map +0 -1
@@ -1,26 +1,34 @@
1
1
  import { mkdir, readFile, writeFile } from 'fs/promises';
2
- import { dirname } from 'path';
2
+ import { dirname, join } from 'path';
3
3
  import simpleGit from 'simple-git';
4
4
  import chalk from 'chalk';
5
- import { getDescriberModelOverride, loadWorkflowSourceInfo, normalizeAgentConfig, resolveAgentRunConfig, } from '../lib/config.js';
5
+ import { getDescriberModelOverride, getReviewAgentIds, loadWorkflowSourceInfo, normalizeAgentConfig, resolveAgentRunConfig, } from '../lib/config.js';
6
6
  import { resolveWithinWorkingDir } from '../lib/path-utils.js';
7
7
  import { parseDiff, getChangedFiles, getFilesWithDiffs } from '../lib/diff-parser.js';
8
- import { parseValidLinesFromPatch } from '../lib/diff-lines.js';
8
+ import { parseDiffLineInfo } from '../lib/diff-lines.js';
9
9
  import { connectToRuntime, executeReview, filterIgnoredFiles, } from '../lib/review-orchestrator.js';
10
- import { ExitError, setExitHandler } from '../lib/exit.js';
11
10
  import { postReviewComments } from '../lib/comment-poster.js';
12
- import { findExistingCommentById } from '../lib/comment-manager.js';
11
+ import { findExistingCommentById, createIssueFingerprint } from '../lib/comment-manager.js';
13
12
  import { removeErrorComment } from '../lib/error-comment-poster.js';
14
13
  import { runDescribeIfEnabled } from '../lib/description-executor.js';
15
14
  import { buildBaseInstructions } from '../lib/review-core.js';
16
15
  import { resolveCursorFixLinkOptions } from '../lib/cursor-fix-link.js';
17
- import { enforceRepoBranchMatch, getCanonicalDiffCommand, resolveBaseBranch, } from '../lib/repository-validator.js';
16
+ import { extractHtmlDocument, parseArtifactOutputPointer, readArtifactOutputPointer, validateHtmlArtifact, } from '../lib/html-artifact.js';
17
+ import { getCanonicalDiffCommand, resolveBaseBranch } from '../lib/repository-validator.js';
18
18
  import { formatCodeQualityReport, generateCodeQualityReport } from '../lib/code-quality-report.js';
19
19
  import { createGitHubClient } from '../github/client.js';
20
20
  import { GitHubPlatformAdapter } from '../github/platform-adapter.js';
21
21
  import { createGitLabClient } from '../gitlab/client.js';
22
22
  import { GitLabPlatformAdapter } from '../gitlab/platform-adapter.js';
23
+ import { loadWorkflowArtifact, saveWorkflowArtifact, updateWorkflowArtifact, workflowArtifactExists, } from '../lib/workflow-artifacts.js';
24
+ import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
23
25
  import { runAgent } from './run-agent.js';
26
+ import { TraceCollector } from '../lib/trace-collector.js';
27
+ import { renderTraceHtml } from '../lib/trace-html.js';
28
+ import { buildWorkflowGraph, formatWorkflowGraphMermaid, } from '../lib/workflow/graph.js';
29
+ import { computeActiveWorkflowNodes, createSkippedWorkflowNodeResult, findWorkflowSegmentIndex, getNodeKind, getNodeNeeds, getWorkflowExecutionOrder, getWorkflowExecutionWaves, getWorkflowNodeSkipReason, getWorkflowNodes, hasWorkflowControlNodes, normalizeWorkflowBooleanLike, renderTemplate, runControlWorkflowNode, splitWorkflowSegments, } from '../lib/workflow/planning.js';
30
+ import { getWorkflowInputConfigType } from '../lib/workflow/input.js';
31
+ export { compileWorkflowPlan, } from '../lib/workflow/compiled-plan.js';
24
32
  function createWorkflowLock() {
25
33
  return { current: Promise.resolve() };
26
34
  }
@@ -76,121 +84,80 @@ function getWorkflowPlatformClient(executionContext, platform) {
76
84
  executionContext.platformClients[platform] = client;
77
85
  return client;
78
86
  }
79
- function getNodeNeeds(node) {
80
- if (node.needs === undefined) {
81
- return [];
82
- }
83
- if (!Array.isArray(node.needs)) {
84
- throw new Error('Workflow node "needs" must be an array of node ids.');
85
- }
86
- return node.needs;
87
- }
88
- function getWorkflowExecutionOrder(nodes) {
89
- const nodeIds = Object.keys(nodes);
90
- const visiting = new Set();
91
- const visited = new Set();
92
- const order = [];
93
- function visit(nodeId) {
94
- if (visited.has(nodeId)) {
95
- return;
96
- }
97
- if (visiting.has(nodeId)) {
98
- throw new Error(`Workflow contains a dependency cycle at node "${nodeId}".`);
99
- }
100
- const node = nodes[nodeId];
101
- if (!node) {
102
- throw new Error(`Workflow references unknown node "${nodeId}".`);
103
- }
104
- visiting.add(nodeId);
105
- for (const dependency of getNodeNeeds(node)) {
106
- if (!nodes[dependency]) {
107
- throw new Error(`Workflow node "${nodeId}" needs unknown node "${dependency}".`);
108
- }
109
- visit(dependency);
110
- }
111
- visiting.delete(nodeId);
112
- visited.add(nodeId);
113
- order.push(nodeId);
114
- }
115
- for (const nodeId of nodeIds) {
116
- visit(nodeId);
117
- }
118
- return order;
119
- }
120
- function getWorkflowNodes(workflowName, workflow) {
121
- const nodes = workflow.nodes;
122
- if (typeof nodes !== 'object' ||
123
- nodes === null ||
124
- Array.isArray(nodes) ||
125
- Object.keys(nodes).length === 0) {
126
- throw new Error(`Workflow "${workflowName}" must define at least one node.`);
127
- }
128
- return nodes;
129
- }
130
- function getWorkflowExecutionWaves(nodes, executionOrder) {
131
- const depthByNode = new Map();
132
- const waves = [];
133
- for (const nodeId of executionOrder) {
134
- const node = nodes[nodeId];
135
- if (!node) {
136
- throw new Error(`Workflow references unknown node "${nodeId}".`);
137
- }
138
- const depth = getNodeNeeds(node).reduce((maxDepth, dependency) => {
139
- return Math.max(maxDepth, (depthByNode.get(dependency) ?? 0) + 1);
140
- }, 0);
141
- depthByNode.set(nodeId, depth);
142
- waves[depth] = waves[depth] ?? [];
143
- waves[depth].push(nodeId);
144
- }
145
- return waves;
146
- }
147
- function getPathValue(root, path) {
148
- return path.split('.').reduce((current, part) => {
149
- if (current === undefined || current === null) {
150
- return undefined;
151
- }
152
- if (typeof current !== 'object') {
153
- return undefined;
154
- }
155
- return current[part];
156
- }, root);
157
- }
158
- function stringifyTemplateValue(value) {
159
- if (value === undefined) {
160
- return '';
161
- }
162
- if (typeof value === 'string') {
163
- return value;
164
- }
165
- return JSON.stringify(value, null, 2);
166
- }
167
- function renderTemplate(template, context) {
168
- return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, rawPath) => {
169
- const path = rawPath.trim();
170
- const value = getPathValue(context, path);
171
- if (value === undefined) {
172
- throw new Error(`Unknown workflow template value "{{${path}}}".`);
173
- }
174
- return stringifyTemplateValue(value);
87
+ async function flushWorkflowTrace(traceCollector, workflowName, inputs, startedAt, workingDir, options) {
88
+ const workflowTrace = traceCollector.buildWorkflowTrace(workflowName, inputs, startedAt);
89
+ const scope = {
90
+ platform: 'local',
91
+ projectId: workflowName,
92
+ subject: 'trace',
93
+ };
94
+ const savedJson = await saveWorkflowArtifact(workingDir, {
95
+ kind: 'trace',
96
+ scope,
97
+ payload: workflowTrace,
175
98
  });
99
+ const traceHtmlPath = join(dirname(savedJson.path), 'trace.html');
100
+ const html = renderTraceHtml(workflowTrace);
101
+ await writeWorkflowFile(workingDir, traceHtmlPath, html);
102
+ if (!options.jsonOutput) {
103
+ console.log(chalk.green(`\n✓ Trace saved to ${savedJson.latestPath}`));
104
+ console.log(chalk.green(`✓ Trace viewer saved to ${traceHtmlPath}`));
105
+ }
176
106
  }
177
107
  async function resolveWorkflowInput(key, input, workingDir) {
178
108
  if (typeof input === 'string') {
179
109
  return input;
180
110
  }
181
- const hasValue = input.value !== undefined;
111
+ const hasValue = input.value !== undefined || input.default !== undefined;
182
112
  const hasFile = input.file !== undefined;
183
113
  if (hasValue && hasFile) {
184
- throw new Error(`Workflow input "${key}" cannot define both value and file.`);
114
+ throw new Error(`Workflow input "${key}" cannot define both value/default and file.`);
185
115
  }
186
116
  if (hasValue) {
187
- return input.value ?? '';
117
+ return String(input.value ?? input.default ?? '');
188
118
  }
189
119
  if (hasFile) {
190
120
  const inputPath = resolveWithinWorkingDir(workingDir, input.file ?? '', 'read');
191
121
  return readFile(inputPath, 'utf-8');
192
122
  }
193
- throw new Error(`Workflow input "${key}" must define value or file.`);
123
+ if (input.required === true) {
124
+ return '';
125
+ }
126
+ return '';
127
+ }
128
+ function validateResolvedWorkflowInput(key, input, value) {
129
+ if (typeof input === 'string') {
130
+ return;
131
+ }
132
+ if (input.required === true && value.trim() === '') {
133
+ throw new Error(`Workflow input "${key}" is required.`);
134
+ }
135
+ const type = getWorkflowInputConfigType(input);
136
+ if (type === 'boolean') {
137
+ if (normalizeWorkflowBooleanLike(value) === undefined) {
138
+ throw new Error(`Workflow input "${key}" must be a boolean value.`);
139
+ }
140
+ return;
141
+ }
142
+ if (type === 'number') {
143
+ if (value.trim() === '' || !Number.isFinite(Number(value))) {
144
+ throw new Error(`Workflow input "${key}" must be a number.`);
145
+ }
146
+ return;
147
+ }
148
+ if (type === 'enum') {
149
+ const allowedValues = input.values?.map(String) ?? [];
150
+ if (allowedValues.length === 0) {
151
+ throw new Error(`Workflow input "${key}" with type enum must define values.`);
152
+ }
153
+ if (!allowedValues.includes(value)) {
154
+ throw new Error(`Workflow input "${key}" must be one of: ${allowedValues.join(', ')}.`);
155
+ }
156
+ return;
157
+ }
158
+ if (type !== 'string') {
159
+ throw new Error(`Workflow input "${key}" has unsupported type "${type}".`);
160
+ }
194
161
  }
195
162
  async function resolveWorkflowInputs(workflow, options, workingDir) {
196
163
  const values = {};
@@ -204,6 +171,9 @@ async function resolveWorkflowInputs(workflow, options, workingDir) {
204
171
  const resolvedPath = resolveWithinWorkingDir(workingDir, filePath, 'read');
205
172
  values[key] = await readFile(resolvedPath, 'utf-8');
206
173
  }
174
+ for (const [key, input] of Object.entries(workflow.inputs ?? {})) {
175
+ validateResolvedWorkflowInput(key, input, values[key] ?? '');
176
+ }
207
177
  return values;
208
178
  }
209
179
  function resolveAgentsFrom(config, agentsFrom) {
@@ -212,17 +182,6 @@ function resolveAgentsFrom(config, agentsFrom) {
212
182
  }
213
183
  throw new Error(`Unsupported workflow agentsFrom "${agentsFrom}". ` + 'Currently supported: review.agents.');
214
184
  }
215
- function getNodeKind(node) {
216
- const configuredKinds = [node.agent, node.agentsFrom, node.action].filter((value) => value !== undefined).length;
217
- if (configuredKinds !== 1) {
218
- throw new Error('Workflow node must define exactly one of agent, agentsFrom, or action.');
219
- }
220
- if (node.agent !== undefined)
221
- return 'agent';
222
- if (node.agentsFrom !== undefined)
223
- return 'agents';
224
- return 'action';
225
- }
226
185
  function hasConfiguredAgentPrompt(config, agentId) {
227
186
  const runConfig = resolveAgentRunConfig(config, agentId);
228
187
  return runConfig.prompt !== undefined || runConfig.promptFile !== undefined;
@@ -247,6 +206,35 @@ async function writeWorkflowFile(workingDir, relativeOutputPath, content) {
247
206
  await mkdir(dirname(outputPath), { recursive: true });
248
207
  await writeFile(outputPath, content, 'utf-8');
249
208
  }
209
+ async function formatWorkflowNodeWriteContent(workingDir, nodeId, writes, content) {
210
+ if (!/\.html?$/i.test(writes)) {
211
+ return content;
212
+ }
213
+ const pointer = parseArtifactOutputPointer(content);
214
+ if (pointer) {
215
+ if (pointer.outputPath !== writes) {
216
+ throw new Error(`Workflow node "${nodeId}" artifact pointer wrote "${pointer.outputPath}" but workflow expected "${writes}".`);
217
+ }
218
+ return readArtifactOutputPointer(workingDir, pointer);
219
+ }
220
+ try {
221
+ const html = extractHtmlDocument(content);
222
+ validateHtmlArtifact(html);
223
+ return html;
224
+ }
225
+ catch (error) {
226
+ try {
227
+ return await readArtifactOutputPointer(workingDir, {
228
+ outputType: 'artifact_output',
229
+ outputPath: writes,
230
+ });
231
+ }
232
+ catch {
233
+ // If the agent did not write a valid artifact itself, surface the response validation error.
234
+ }
235
+ throw new Error(`Workflow node "${nodeId}" produced invalid HTML output: ${error instanceof Error ? error.message : String(error)}`);
236
+ }
237
+ }
250
238
  function renderNodeWritesPath(nodeId, node, context) {
251
239
  if (!node.writes) {
252
240
  return undefined;
@@ -257,7 +245,7 @@ function renderNodeWritesPath(nodeId, node, context) {
257
245
  }
258
246
  return writes;
259
247
  }
260
- async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, context) {
248
+ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
261
249
  const agentId = node.agent;
262
250
  if (!agentId) {
263
251
  throw new Error(`Workflow node "${nodeId}" is missing agent.`);
@@ -267,17 +255,25 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
267
255
  throw new Error(`Workflow agent node "${nodeId}" must define input or configure ` +
268
256
  `agents.overrides.${agentId}.run.prompt/promptFile.`);
269
257
  }
270
- const result = await runAgent(config, agentId, createAgentOptions(prompt, options, workingDir));
258
+ const agentOptions = createAgentOptions(prompt, options, workingDir);
259
+ if (executionContext?.traceCollector && prompt) {
260
+ agentOptions.traceCollector = executionContext.traceCollector;
261
+ executionContext.traceCollector.setContext(nodeId, agentId, prompt);
262
+ }
263
+ const result = await runAgent(config, agentId, agentOptions);
271
264
  const writes = renderNodeWritesPath(nodeId, node, context);
265
+ const output = writes
266
+ ? await formatWorkflowNodeWriteContent(workingDir, nodeId, writes, node.json === true ? JSON.stringify(result, null, 2) : result.response)
267
+ : result.response;
272
268
  if (writes) {
273
- await writeWorkflowFile(workingDir, writes, node.json === true ? JSON.stringify(result, null, 2) : result.response);
269
+ await writeWorkflowFile(workingDir, writes, output);
274
270
  }
275
271
  return {
276
272
  id: nodeId,
277
273
  type: 'agent',
278
274
  agent: agentId,
279
275
  response: result.response,
280
- output: result.response,
276
+ output,
281
277
  writes,
282
278
  };
283
279
  }
@@ -323,9 +319,64 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
323
319
  if (node.action === 'git-add') {
324
320
  return runGitAddWorkflowNode(nodeId, node, workingDir, context, executionContext);
325
321
  }
322
+ if (node.action === 'git-branch') {
323
+ return runGitBranchWorkflowNode(nodeId, node, workingDir, context, executionContext);
324
+ }
326
325
  if (node.action === 'git-commit') {
327
326
  return runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext);
328
327
  }
328
+ if (node.action === 'git-push') {
329
+ return runGitPushWorkflowNode(nodeId, node, workingDir, context, executionContext);
330
+ }
331
+ if (node.action === 'has-diff') {
332
+ return runHasDiffWorkflowNode(nodeId, node, workingDir, context, executionContext);
333
+ }
334
+ if (node.action === 'stack-guard') {
335
+ return runStackGuardWorkflowNode(nodeId, node, context);
336
+ }
337
+ if (node.action === 'review-threshold') {
338
+ return runReviewThresholdWorkflowNode(nodeId, node, context);
339
+ }
340
+ if (node.action === 'save-artifact') {
341
+ return runSaveArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext);
342
+ }
343
+ if (node.action === 'load-artifact') {
344
+ return runLoadArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext);
345
+ }
346
+ if (node.action === 'artifact-exists') {
347
+ return runArtifactExistsWorkflowNode(nodeId, node, workingDir, context, executionContext);
348
+ }
349
+ if (node.action === 'create-review-artifact') {
350
+ return runCreateReviewArtifactWorkflowNode(nodeId, node, context);
351
+ }
352
+ if (node.action === 'review-artifact-status') {
353
+ return runReviewArtifactStatusWorkflowNode(nodeId, node, context);
354
+ }
355
+ if (node.action === 'review-artifact-add-finding') {
356
+ return runReviewArtifactAddFindingWorkflowNode(nodeId, node, workingDir, context);
357
+ }
358
+ if (node.action === 'review-artifact-update-findings') {
359
+ return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context);
360
+ }
361
+ if (node.action === 'verify-fix') {
362
+ return runVerifyFixWorkflowNode(nodeId, node, workingDir, context);
363
+ }
364
+ if (node.action === 'review-artifact-promote-finding') {
365
+ return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, {
366
+ disposition: 'confirmed',
367
+ });
368
+ }
369
+ if (node.action === 'review-artifact-resolve-finding') {
370
+ return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, {
371
+ state: 'resolved',
372
+ disposition: 'resolved',
373
+ });
374
+ }
375
+ if (node.action === 'create-change-request' ||
376
+ node.action === 'create-pr' ||
377
+ node.action === 'create-mr') {
378
+ return runCreateChangeRequestWorkflowNode(nodeId, node, context, executionContext);
379
+ }
329
380
  if (node.action === 'change-source') {
330
381
  return runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext);
331
382
  }
@@ -347,6 +398,9 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
347
398
  if (node.action === 'post-review-comments') {
348
399
  return runPostReviewCommentsWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
349
400
  }
401
+ if (node.action === 'post-fix-status') {
402
+ return runPostFixStatusWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
403
+ }
350
404
  throw new Error(`Unsupported workflow action "${node.action}" in node "${nodeId}".`);
351
405
  }
352
406
  async function runWriteWorkflowNode(nodeId, node, workingDir, context) {
@@ -371,9 +425,13 @@ async function runWriteWorkflowNode(nodeId, node, workingDir, context) {
371
425
  writes: relativeOutputPath,
372
426
  };
373
427
  }
374
- function getBooleanActionOption(node, key) {
428
+ function getBooleanActionOption(node, key, context) {
375
429
  const value = node.with?.[key];
376
- return value === true || value === 'true';
430
+ if (typeof value === 'string' && context) {
431
+ const rendered = renderTemplate(value, context).trim().toLowerCase();
432
+ return rendered === 'true' || rendered === '1' || rendered === 'yes';
433
+ }
434
+ return value === true || value === 'true' || value === 1;
377
435
  }
378
436
  function getStringActionOption(node, key, context) {
379
437
  const value = node.with?.[key];
@@ -419,6 +477,12 @@ function getPathActionOption(nodeId, node, context, workingDir) {
419
477
  }
420
478
  return paths;
421
479
  }
480
+ function getOptionalPathActionOption(nodeId, node, context, workingDir) {
481
+ if (!hasActionOption(node, 'paths') && !hasActionOption(node, 'path')) {
482
+ return undefined;
483
+ }
484
+ return getPathActionOption(nodeId, node, context, workingDir);
485
+ }
422
486
  async function requireWorkflowGitRepo(nodeId, workingDir, executionContext) {
423
487
  const git = getWorkflowGitClient(executionContext, workingDir);
424
488
  const isRepo = await git.checkIsRepo();
@@ -433,7 +497,7 @@ async function runGitDiffWorkflowNode(nodeId, node, workingDir, context, executi
433
497
  if (!isRepo) {
434
498
  throw new Error(`Workflow git-diff node "${nodeId}" must run from a git repository.`);
435
499
  }
436
- const staged = getBooleanActionOption(node, 'staged');
500
+ const staged = getBooleanActionOption(node, 'staged', context);
437
501
  const diff = staged ? await git.diff(['--cached']) : await git.diff();
438
502
  const writes = renderNodeWritesPath(nodeId, node, context);
439
503
  if (writes) {
@@ -460,6 +524,24 @@ async function runGitAddWorkflowNode(nodeId, node, workingDir, context, executio
460
524
  output: paths,
461
525
  };
462
526
  }
527
+ async function runGitBranchWorkflowNode(nodeId, node, workingDir, context, executionContext) {
528
+ const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
529
+ const name = requireStringActionOption(nodeId, node, 'name', context);
530
+ const from = getStringActionOption(node, 'from', context)?.trim();
531
+ const force = getBooleanActionOption(node, 'force', context);
532
+ const args = ['checkout', force ? '-B' : '-b', name];
533
+ if (from) {
534
+ args.push(from);
535
+ }
536
+ await git.raw(args);
537
+ return {
538
+ id: nodeId,
539
+ type: 'action',
540
+ action: node.action,
541
+ response: `checked out branch ${name}`,
542
+ output: { branch: name, from, force },
543
+ };
544
+ }
463
545
  async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext) {
464
546
  const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
465
547
  const message = requireStringActionOption(nodeId, node, 'message', context);
@@ -484,13 +566,464 @@ async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, execu
484
566
  output,
485
567
  };
486
568
  }
487
- async function loadLocalChangeSource(nodeId, node, workingDir) {
569
+ async function runGitPushWorkflowNode(nodeId, node, workingDir, context, executionContext) {
570
+ const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
571
+ const configuredRemote = getStringActionOption(node, 'remote', context)?.trim();
572
+ const remote = configuredRemote && configuredRemote.length > 0 ? configuredRemote : 'origin';
573
+ const branch = requireStringActionOption(nodeId, node, 'branch', context);
574
+ const configuredRemoteBranch = getStringActionOption(node, 'remoteBranch', context)?.trim();
575
+ const remoteBranch = configuredRemoteBranch && configuredRemoteBranch.length > 0 ? configuredRemoteBranch : branch;
576
+ const setUpstream = !hasActionOption(node, 'setUpstream') || getBooleanActionOption(node, 'setUpstream', context);
577
+ const force = getBooleanActionOption(node, 'force', context);
578
+ const args = ['push'];
579
+ if (setUpstream) {
580
+ args.push('-u');
581
+ }
582
+ if (force) {
583
+ args.push('--force-with-lease');
584
+ }
585
+ args.push(remote, `${branch}:${remoteBranch}`);
586
+ await git.raw(args);
587
+ return {
588
+ id: nodeId,
589
+ type: 'action',
590
+ action: node.action,
591
+ response: `pushed ${branch} to ${remote}/${remoteBranch}`,
592
+ output: { remote, branch, remoteBranch, setUpstream, force },
593
+ };
594
+ }
595
+ async function runHasDiffWorkflowNode(nodeId, node, workingDir, context, executionContext) {
596
+ const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
597
+ const paths = getOptionalPathActionOption(nodeId, node, context, workingDir);
598
+ const diff = paths ? await git.diff(['--', ...paths]) : await git.diff();
599
+ const files = paths ?? [];
600
+ const changed = diff.trim().length > 0;
601
+ return {
602
+ id: nodeId,
603
+ type: 'action',
604
+ action: node.action,
605
+ response: changed ? 'changes found' : 'no changes found',
606
+ output: { changed, files, bytes: Buffer.byteLength(diff, 'utf8') },
607
+ };
608
+ }
609
+ function runStackGuardWorkflowNode(nodeId, node, context) {
610
+ const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
611
+ const source = context.artifacts[sourceArtifact];
612
+ if (!isReviewSource(source)) {
613
+ throw new Error(`Workflow stack-guard node "${nodeId}" needs a ReviewSource artifact.`);
614
+ }
615
+ const pullRequest = isPullRequest(source.context.pullRequest)
616
+ ? source.context.pullRequest
617
+ : undefined;
618
+ const sourceBranch = pullRequest?.sourceBranch ?? '';
619
+ const allowStackedSource = getBooleanActionOption(node, 'allowStackedSource', context);
620
+ const rawPrefixes = getStringActionOption(node, 'reservedPrefixes', context) ?? 'drs-fix/,drs-guidance/,drs-stack/';
621
+ const reservedPrefixes = rawPrefixes
622
+ .split(/[,\n]/)
623
+ .map((prefix) => prefix.trim())
624
+ .filter(Boolean);
625
+ const matchingPrefix = reservedPrefixes.find((prefix) => sourceBranch.startsWith(prefix));
626
+ const allowed = allowStackedSource || !matchingPrefix;
627
+ const reason = allowed ? 'allowed' : `source branch uses reserved DRS prefix "${matchingPrefix}"`;
628
+ return {
629
+ id: nodeId,
630
+ type: 'action',
631
+ action: node.action,
632
+ response: reason,
633
+ output: { allowed, reason, sourceBranch, reservedPrefixes },
634
+ };
635
+ }
636
+ function severityRank(severity) {
637
+ const normalized = severity.trim().toUpperCase();
638
+ if (normalized === 'CRITICAL')
639
+ return 4;
640
+ if (normalized === 'HIGH')
641
+ return 3;
642
+ if (normalized === 'MEDIUM')
643
+ return 2;
644
+ if (normalized === 'LOW')
645
+ return 1;
646
+ return 0;
647
+ }
648
+ function runReviewThresholdWorkflowNode(nodeId, node, context) {
649
+ const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
650
+ const reviewResult = context.artifacts[reviewArtifact];
651
+ if (!isReviewResult(reviewResult)) {
652
+ throw new Error(`Workflow review-threshold node "${nodeId}" needs a ReviewResult artifact.`);
653
+ }
654
+ const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
655
+ const minIssues = hasActionOption(node, 'minIssues')
656
+ ? requireNumberActionOption(nodeId, node, 'minIssues', context)
657
+ : 1;
658
+ const thresholdRank = severityRank(severity);
659
+ if (thresholdRank === 0) {
660
+ throw new Error(`Workflow review-threshold node "${nodeId}" has unsupported severity "${severity}".`);
661
+ }
662
+ const matchingIssues = reviewResult.issues.filter((issue) => severityRank(issue.severity) >= thresholdRank);
663
+ const matched = matchingIssues.length >= minIssues;
664
+ return {
665
+ id: nodeId,
666
+ type: 'action',
667
+ action: node.action,
668
+ response: matched ? `${matchingIssues.length} matching issue(s)` : 'threshold not met',
669
+ output: { matched, count: matchingIssues.length, severity, minIssues },
670
+ };
671
+ }
672
+ async function getCurrentBranch(workingDir, executionContext) {
673
+ const git = await requireWorkflowGitRepo('artifact-scope', workingDir, executionContext);
674
+ const branch = await git.branch();
675
+ return branch.current ?? 'unknown';
676
+ }
677
+ async function resolveArtifactScope(nodeId, node, workingDir, context, executionContext) {
678
+ const sourceArtifact = getStringActionOption(node, 'source', context);
679
+ const source = sourceArtifact ? context.artifacts[sourceArtifact] : undefined;
680
+ const reviewSource = isReviewSource(source) ? source : undefined;
681
+ const sourceTarget = readSourcePostTarget(reviewSource);
682
+ const explicitPlatform = getStringActionOption(node, 'platform', context);
683
+ const platform = explicitPlatform ?? sourceTarget.platform ?? 'local';
684
+ const projectId = getStringActionOption(node, 'project', context) ??
685
+ getStringActionOption(node, 'projectId', context) ??
686
+ (hasActionOption(node, 'owner') || hasActionOption(node, 'repo')
687
+ ? `${requireStringActionOption(nodeId, node, 'owner', context)}/${requireStringActionOption(nodeId, node, 'repo', context)}`
688
+ : undefined) ??
689
+ sourceTarget.projectId ??
690
+ 'local';
691
+ const explicitSubject = getStringActionOption(node, 'subject', context)?.trim();
692
+ if (explicitSubject) {
693
+ return { platform, projectId, subject: explicitSubject };
694
+ }
695
+ const explicitChangeKind = getStringActionOption(node, 'changeKind', context)?.trim();
696
+ const explicitChangeNumber = getStringActionOption(node, 'changeNumber', context)?.trim() ??
697
+ getStringActionOption(node, 'pr', context)?.trim() ??
698
+ getStringActionOption(node, 'mr', context)?.trim();
699
+ const sourceChangeKind = sourceTarget.platform === 'gitlab' ? 'mr' : sourceTarget.platform ? 'pr' : undefined;
700
+ const sourceChangeNumber = sourceTarget.prNumber;
701
+ const changeKind = explicitChangeKind && explicitChangeKind.length > 0 ? explicitChangeKind : sourceChangeKind;
702
+ const changeNumber = explicitChangeNumber ?? sourceChangeNumber;
703
+ if (changeKind && changeNumber !== undefined) {
704
+ return { platform, projectId, changeKind, changeNumber };
705
+ }
706
+ const configuredBranch = getStringActionOption(node, 'branch', context)?.trim();
707
+ const branch = configuredBranch && configuredBranch.length > 0
708
+ ? configuredBranch
709
+ : await getCurrentBranch(workingDir, executionContext);
710
+ return { platform, projectId, branch };
711
+ }
712
+ function getArtifactPayloadFromNode(nodeId, node, context) {
713
+ const artifactName = getStringActionOption(node, 'artifact', context);
714
+ if (artifactName) {
715
+ if (!Object.prototype.hasOwnProperty.call(context.artifacts, artifactName)) {
716
+ throw new Error(`Workflow node "${nodeId}" references unknown artifact "${artifactName}".`);
717
+ }
718
+ return context.artifacts[artifactName];
719
+ }
720
+ const payloadName = getStringActionOption(node, 'payload', context);
721
+ if (payloadName && Object.prototype.hasOwnProperty.call(context.artifacts, payloadName)) {
722
+ return context.artifacts[payloadName];
723
+ }
724
+ if (payloadName) {
725
+ try {
726
+ return JSON.parse(payloadName);
727
+ }
728
+ catch {
729
+ return payloadName;
730
+ }
731
+ }
732
+ throw new Error(`Workflow node "${nodeId}" must define with.artifact or with.payload.`);
733
+ }
734
+ async function runSaveArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext) {
735
+ const kind = requireStringActionOption(nodeId, node, 'kind', context);
736
+ const payload = getArtifactPayloadFromNode(nodeId, node, context);
737
+ const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
738
+ const saved = await saveWorkflowArtifact(workingDir, { kind, scope, payload });
739
+ return {
740
+ id: nodeId,
741
+ type: 'action',
742
+ action: node.action,
743
+ response: `saved ${kind} artifact ${saved.artifact.id}`,
744
+ output: { ...saved.artifact, path: saved.path, latestPath: saved.latestPath },
745
+ };
746
+ }
747
+ async function runLoadArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext) {
748
+ const kind = requireStringActionOption(nodeId, node, 'kind', context);
749
+ const rawId = getStringActionOption(node, 'id', context)?.trim();
750
+ const id = rawId && rawId.length > 0 ? rawId : undefined;
751
+ const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
752
+ const loaded = await loadWorkflowArtifact(workingDir, kind, scope, id);
753
+ return {
754
+ id: nodeId,
755
+ type: 'action',
756
+ action: node.action,
757
+ response: `loaded ${kind} artifact ${loaded.artifact.id}`,
758
+ output: { ...loaded.artifact, path: loaded.path },
759
+ };
760
+ }
761
+ async function runArtifactExistsWorkflowNode(nodeId, node, workingDir, context, executionContext) {
762
+ const kind = requireStringActionOption(nodeId, node, 'kind', context);
763
+ const rawId = getStringActionOption(node, 'id', context)?.trim();
764
+ const id = rawId && rawId.length > 0 ? rawId : undefined;
765
+ const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
766
+ const exists = await workflowArtifactExists(workingDir, kind, scope, id);
767
+ return {
768
+ id: nodeId,
769
+ type: 'action',
770
+ action: node.action,
771
+ response: exists ? 'artifact exists' : 'artifact missing',
772
+ output: { exists, kind, scope, id },
773
+ };
774
+ }
775
+ function runCreateReviewArtifactWorkflowNode(nodeId, node, context) {
776
+ const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
777
+ const review = context.artifacts[reviewArtifact];
778
+ if (!isReviewResult(review)) {
779
+ throw new Error(`Workflow create-review-artifact node "${nodeId}" needs a ReviewResult artifact.`);
780
+ }
781
+ const sourceArtifact = getStringActionOption(node, 'source', context);
782
+ const source = sourceArtifact ? context.artifacts[sourceArtifact] : undefined;
783
+ const reviewSource = isReviewSource(source) ? source : undefined;
784
+ const artifact = createReviewArtifactPayload(review, reviewSource);
785
+ return {
786
+ id: nodeId,
787
+ type: 'action',
788
+ action: node.action,
789
+ response: `created review artifact ${artifact.reviewId}`,
790
+ output: artifact,
791
+ };
792
+ }
793
+ function getReviewArtifactPayloadFromValue(nodeId, value) {
794
+ const envelope = value;
795
+ const payload = envelope && typeof envelope === 'object' && 'payload' in envelope ? envelope.payload : value;
796
+ if (!isReviewArtifactPayload(payload)) {
797
+ throw new Error(`Workflow review-artifact-status node "${nodeId}" needs a review artifact payload.`);
798
+ }
799
+ return payload;
800
+ }
801
+ function getReviewArtifactEnvelopeFromValue(value) {
802
+ if (!value || typeof value !== 'object') {
803
+ return undefined;
804
+ }
805
+ const envelope = value;
806
+ if (envelope.schemaVersion === 1 &&
807
+ envelope.kind === 'review' &&
808
+ typeof envelope.id === 'string' &&
809
+ typeof envelope.createdAt === 'string' &&
810
+ typeof envelope.updatedAt === 'string' &&
811
+ envelope.scope &&
812
+ typeof envelope.scope === 'object' &&
813
+ isReviewArtifactPayload(envelope.payload)) {
814
+ return envelope;
815
+ }
816
+ return undefined;
817
+ }
818
+ function getReviewArtifactInput(nodeId, node, context) {
819
+ const artifactName = getStringActionOption(node, 'artifact', context) ?? 'reviewArtifact';
820
+ const artifactValue = context.artifacts[artifactName];
821
+ const artifact = getReviewArtifactPayloadFromValue(nodeId, artifactValue);
822
+ return { artifact, envelope: getReviewArtifactEnvelopeFromValue(artifactValue) };
823
+ }
824
+ async function persistMutatedReviewArtifact(workingDir, payload, envelope) {
825
+ if (!envelope) {
826
+ return { output: payload, responseSuffix: '' };
827
+ }
828
+ const saved = await updateWorkflowArtifact(workingDir, { artifact: envelope, payload });
829
+ return {
830
+ output: { ...saved.artifact, path: saved.path, latestPath: saved.latestPath },
831
+ responseSuffix: ` and saved ${saved.artifact.id}`,
832
+ };
833
+ }
834
+ function parseListActionOption(node, key, context) {
835
+ const value = getStringActionOption(node, key, context)?.trim();
836
+ if (!value) {
837
+ return undefined;
838
+ }
839
+ return value
840
+ .split(/[,\n]/)
841
+ .map((item) => item.trim())
842
+ .filter(Boolean);
843
+ }
844
+ function parseReviewFindingState(nodeId, value) {
845
+ if (!value) {
846
+ return undefined;
847
+ }
848
+ if (value === 'open' || value === 'attempted' || value === 'resolved') {
849
+ return value;
850
+ }
851
+ throw new Error(`Workflow node "${nodeId}" has invalid review finding state "${value}".`);
852
+ }
853
+ function parseReviewFindingDisposition(nodeId, value) {
854
+ if (!value) {
855
+ return undefined;
856
+ }
857
+ if (value === 'confirmed' ||
858
+ value === 'uncertain' ||
859
+ value === 'pre_existing' ||
860
+ value === 'partial' ||
861
+ value === 'still_open' ||
862
+ value === 'regression' ||
863
+ value === 'resolved') {
864
+ return value;
865
+ }
866
+ throw new Error(`Workflow node "${nodeId}" has invalid review finding disposition "${value}".`);
867
+ }
868
+ function parseReviewFindingSource(nodeId, value) {
869
+ if (!value) {
870
+ return 'manual';
871
+ }
872
+ if (value === 'agent' || value === 'manual' || value === 'external') {
873
+ return value;
874
+ }
875
+ throw new Error(`Workflow node "${nodeId}" has invalid review finding source "${value}".`);
876
+ }
877
+ function isReviewIssue(value) {
878
+ if (!value || typeof value !== 'object') {
879
+ return false;
880
+ }
881
+ const issue = value;
882
+ return (typeof issue.severity === 'string' &&
883
+ typeof issue.category === 'string' &&
884
+ typeof issue.title === 'string' &&
885
+ typeof issue.file === 'string' &&
886
+ typeof issue.problem === 'string' &&
887
+ typeof issue.solution === 'string');
888
+ }
889
+ function getReviewIssueFromNode(nodeId, node, context) {
890
+ const issueName = requireStringActionOption(nodeId, node, 'issue', context);
891
+ const issue = Object.prototype.hasOwnProperty.call(context.artifacts, issueName)
892
+ ? context.artifacts[issueName]
893
+ : JSON.parse(issueName);
894
+ if (!isReviewIssue(issue)) {
895
+ throw new Error(`Workflow review-artifact-add-finding node "${nodeId}" needs a ReviewIssue.`);
896
+ }
897
+ return issue;
898
+ }
899
+ function runReviewArtifactStatusWorkflowNode(nodeId, node, context) {
900
+ const artifactName = getStringActionOption(node, 'artifact', context) ?? 'reviewArtifact';
901
+ const artifactValue = context.artifacts[artifactName];
902
+ const artifact = getReviewArtifactPayloadFromValue(nodeId, artifactValue);
903
+ const status = getReviewArtifactStatus(artifact);
904
+ return {
905
+ id: nodeId,
906
+ type: 'action',
907
+ action: node.action,
908
+ response: `${status.totalFindings} finding(s), ${status.openFindings} open`,
909
+ output: status,
910
+ };
911
+ }
912
+ async function runReviewArtifactAddFindingWorkflowNode(nodeId, node, workingDir, context) {
913
+ const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
914
+ const issue = getReviewIssueFromNode(nodeId, node, context);
915
+ const source = parseReviewFindingSource(nodeId, getStringActionOption(node, 'source', context));
916
+ const updated = addReviewArtifactFinding(artifact, issue, source);
917
+ const persisted = await persistMutatedReviewArtifact(workingDir, updated, envelope);
918
+ const addedFinding = updated.findings[updated.findings.length - 1];
919
+ return {
920
+ id: nodeId,
921
+ type: 'action',
922
+ action: node.action,
923
+ response: `added review finding ${addedFinding?.id ?? 'unknown'}${persisted.responseSuffix}`,
924
+ output: persisted.output,
925
+ };
926
+ }
927
+ async function runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, defaults = {}) {
928
+ const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
929
+ const state = parseReviewFindingState(nodeId, getStringActionOption(node, 'state', context) ?? defaults.state);
930
+ const disposition = parseReviewFindingDisposition(nodeId, getStringActionOption(node, 'disposition', context) ?? defaults.disposition);
931
+ if (!state && !disposition) {
932
+ throw new Error(`Workflow node "${nodeId}" must define with.state or with.disposition.`);
933
+ }
934
+ const { artifact: updated, updatedIds } = updateReviewArtifactFindings(artifact, {
935
+ ids: parseListActionOption(node, 'ids', context),
936
+ fingerprints: parseListActionOption(node, 'fingerprints', context),
937
+ severity: getStringActionOption(node, 'severity', context)?.trim().toUpperCase(),
938
+ state,
939
+ disposition,
940
+ });
941
+ const persisted = await persistMutatedReviewArtifact(workingDir, updated, envelope);
942
+ return {
943
+ id: nodeId,
944
+ type: 'action',
945
+ action: node.action,
946
+ response: `updated ${updatedIds.length} review finding(s)${persisted.responseSuffix}`,
947
+ output: {
948
+ ...(typeof persisted.output === 'object' && persisted.output !== null
949
+ ? persisted.output
950
+ : {}),
951
+ updatedIds,
952
+ },
953
+ };
954
+ }
955
+ async function runVerifyFixWorkflowNode(nodeId, node, workingDir, context) {
956
+ const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
957
+ const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'reReview';
958
+ const review = context.artifacts[reviewArtifact];
959
+ if (!isReviewResult(review)) {
960
+ throw new Error(`Workflow verify-fix node "${nodeId}" needs a ReviewResult.`);
961
+ }
962
+ const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
963
+ if (severityRank(severity) === 0) {
964
+ throw new Error(`Workflow verify-fix node "${nodeId}" has unsupported severity "${severity}".`);
965
+ }
966
+ const minIssues = hasActionOption(node, 'minIssues')
967
+ ? requireNumberActionOption(nodeId, node, 'minIssues', context)
968
+ : 1;
969
+ const fixChangeName = getStringActionOption(node, 'fixChange', context);
970
+ const fixChange = fixChangeName ? context.artifacts[fixChangeName] : undefined;
971
+ const fixSource = isReviewSource(fixChange) ? fixChange : undefined;
972
+ const reconciliation = reconcileReviewArtifactFindings(artifact, review, {
973
+ severity,
974
+ minIssues,
975
+ fixSource,
976
+ });
977
+ const persisted = await persistMutatedReviewArtifact(workingDir, reconciliation.artifact, envelope);
978
+ const status = getReviewArtifactStatus(reconciliation.artifact);
979
+ const output = {
980
+ ...(typeof persisted.output === 'object' && persisted.output !== null ? persisted.output : {}),
981
+ payload: reconciliation.artifact,
982
+ verification: {
983
+ severity,
984
+ minIssues,
985
+ shouldContinue: reconciliation.shouldContinue,
986
+ actionableOpen: reconciliation.actionableOpen,
987
+ fixFiles: fixSource?.files.length ?? 0,
988
+ resolved: reconciliation.resolved,
989
+ partial: reconciliation.partial,
990
+ stillOpen: reconciliation.stillOpen,
991
+ regression: reconciliation.regression,
992
+ statuses: reconciliation.statuses.map((statusItem) => ({
993
+ id: statusItem.finding.id,
994
+ fingerprint: statusItem.finding.fingerprint,
995
+ disposition: statusItem.disposition,
996
+ severity: statusItem.finding.issue.severity,
997
+ file: statusItem.finding.issue.file,
998
+ line: statusItem.finding.issue.line,
999
+ title: statusItem.finding.issue.title,
1000
+ verificationMissing: statusItem.verificationMissing === true,
1001
+ verificationRationale: statusItem.finding.verification?.rationale,
1002
+ })),
1003
+ },
1004
+ shouldContinue: reconciliation.shouldContinue,
1005
+ actionableOpen: reconciliation.actionableOpen,
1006
+ fixFiles: fixSource?.files.length ?? 0,
1007
+ updatedIds: reconciliation.statuses.map((statusItem) => statusItem.finding.id),
1008
+ status,
1009
+ };
1010
+ return {
1011
+ id: nodeId,
1012
+ type: 'action',
1013
+ action: node.action,
1014
+ response: reconciliation.shouldContinue
1015
+ ? `${reconciliation.actionableOpen} actionable finding(s) remain${persisted.responseSuffix}`
1016
+ : `fix verification converged${persisted.responseSuffix}`,
1017
+ output,
1018
+ };
1019
+ }
1020
+ async function loadLocalChangeSource(nodeId, node, workingDir, context) {
488
1021
  const git = simpleGit({ baseDir: workingDir });
489
1022
  const isRepo = await git.checkIsRepo();
490
1023
  if (!isRepo) {
491
1024
  throw new Error(`Workflow change-source node "${nodeId}" must run from a git repository.`);
492
1025
  }
493
- const staged = getBooleanActionOption(node, 'staged');
1026
+ const staged = getBooleanActionOption(node, 'staged', context);
494
1027
  const diffText = staged ? await git.diff(['--cached']) : await git.diff();
495
1028
  const diffs = parseDiff(diffText);
496
1029
  const changedFiles = getChangedFiles(diffs);
@@ -528,8 +1061,8 @@ async function resolveGitRangeToRef(git) {
528
1061
  function isStableSemverTag(tag) {
529
1062
  return /^v?\d+\.\d+\.\d+$/.test(tag);
530
1063
  }
531
- async function resolvePreviousGitRangeTag(nodeId, node, git, toRef) {
532
- const includePrerelease = getBooleanActionOption(node, 'includePrereleaseFrom');
1064
+ async function resolvePreviousGitRangeTag(nodeId, node, git, toRef, context) {
1065
+ const includePrerelease = getBooleanActionOption(node, 'includePrereleaseFrom', context);
533
1066
  const tagOutput = await git.raw(['tag', '--merged', toRef, '--sort=-v:refname']);
534
1067
  const tags = tagOutput
535
1068
  .split('\n')
@@ -544,11 +1077,9 @@ async function resolvePreviousGitRangeTag(nodeId, node, git, toRef) {
544
1077
  }
545
1078
  async function resolveGitRangeRefs(nodeId, node, context, git) {
546
1079
  const configuredToRef = getStringActionOption(node, 'to', context)?.trim();
547
- const toRef = configuredToRef ? configuredToRef : await resolveGitRangeToRef(git);
1080
+ const toRef = configuredToRef ?? (await resolveGitRangeToRef(git));
548
1081
  const configuredFromRef = getStringActionOption(node, 'from', context)?.trim();
549
- const fromRef = configuredFromRef
550
- ? configuredFromRef
551
- : await resolvePreviousGitRangeTag(nodeId, node, git, toRef);
1082
+ const fromRef = configuredFromRef ?? (await resolvePreviousGitRangeTag(nodeId, node, git, toRef, context));
552
1083
  if (!fromRef) {
553
1084
  throw new Error(`Workflow node "${nodeId}" could not infer the previous tag for ${toRef}. ` +
554
1085
  'Provide with.from explicitly.');
@@ -619,11 +1150,68 @@ async function loadGitLabChangeSource(nodeId, node, workingDir, context, executi
619
1150
  ]);
620
1151
  return createPlatformChangeSource('gitlab', `GitLab MR ${projectId}!${mrIid}`, projectId, pullRequest, changedFiles, workingDir);
621
1152
  }
1153
+ function combineVerificationPatch(originalPatch, fixPatch) {
1154
+ const sections = [];
1155
+ if (originalPatch) {
1156
+ sections.push(`# Original PR/MR diff\n${originalPatch}`);
1157
+ }
1158
+ if (fixPatch) {
1159
+ sections.push(`# Local fix diff\n${fixPatch}`);
1160
+ }
1161
+ return sections.join('\n\n');
1162
+ }
1163
+ async function loadFixVerificationChangeSource(nodeId, node, workingDir, context) {
1164
+ const sourceName = getStringActionOption(node, 'source', context) ?? 'change';
1165
+ const source = context.artifacts[sourceName];
1166
+ if (!isReviewSource(source)) {
1167
+ throw new Error(`Workflow fix-verification change-source node "${nodeId}" needs a source ReviewSource artifact.`);
1168
+ }
1169
+ const fixChangeName = getStringActionOption(node, 'fixChange', context) ?? 'fixChange';
1170
+ const fixChange = context.artifacts[fixChangeName];
1171
+ if (!isReviewSource(fixChange)) {
1172
+ throw new Error(`Workflow fix-verification change-source node "${nodeId}" needs a fixChange ReviewSource artifact.`);
1173
+ }
1174
+ const files = [...new Set([...source.files, ...fixChange.files])];
1175
+ const pullRequest = isPullRequest(source.context.pullRequest) ? source.context.pullRequest : null;
1176
+ const baseRef = pullRequest?.targetBranch ?? source.context.baseBranch ?? 'HEAD~1';
1177
+ const realDiffs = await tryGetRealPostFixDiff(workingDir, files, baseRef);
1178
+ return {
1179
+ ...source,
1180
+ name: `${source.name} with local fixes`,
1181
+ files,
1182
+ filesWithDiffs: realDiffs ??
1183
+ files.map((filename) => ({
1184
+ filename,
1185
+ patch: combineVerificationPatch((source.filesWithDiffs ?? []).find((f) => f.filename === filename)?.patch, (fixChange.filesWithDiffs ?? []).find((f) => f.filename === filename)?.patch),
1186
+ })),
1187
+ context: {
1188
+ ...source.context,
1189
+ sourceType: 'fix-verification',
1190
+ fixFiles: fixChange.files,
1191
+ },
1192
+ staged: fixChange.staged,
1193
+ };
1194
+ }
1195
+ async function tryGetRealPostFixDiff(workingDir, files, baseRef) {
1196
+ try {
1197
+ const git = simpleGit({ baseDir: workingDir });
1198
+ const isRepo = await git.checkIsRepo();
1199
+ if (!isRepo)
1200
+ return undefined;
1201
+ const diffText = await git.diff(['--no-ext-diff', '-M', baseRef, '--', ...files]);
1202
+ if (!diffText.trim())
1203
+ return undefined;
1204
+ return getFilesWithDiffs(parseDiff(diffText));
1205
+ }
1206
+ catch {
1207
+ return undefined;
1208
+ }
1209
+ }
622
1210
  async function runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext) {
623
1211
  const type = getStringActionOption(node, 'type', context) ?? 'local';
624
1212
  let source;
625
1213
  if (type === 'local') {
626
- source = await loadLocalChangeSource(nodeId, node, workingDir);
1214
+ source = await loadLocalChangeSource(nodeId, node, workingDir, context);
627
1215
  }
628
1216
  else if (type === 'git-range') {
629
1217
  source = await loadGitRangeChangeSource(nodeId, node, workingDir, context, executionContext);
@@ -634,9 +1222,12 @@ async function runChangeSourceWorkflowNode(nodeId, node, workingDir, context, ex
634
1222
  else if (type === 'gitlab-mr') {
635
1223
  source = await loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext);
636
1224
  }
1225
+ else if (type === 'fix-verification') {
1226
+ source = await loadFixVerificationChangeSource(nodeId, node, workingDir, context);
1227
+ }
637
1228
  else {
638
1229
  throw new Error(`Unsupported workflow change-source type "${type}" in node "${nodeId}". ` +
639
- 'Currently supported: local, git-range, github-pr, gitlab-mr.');
1230
+ 'Currently supported: local, git-range, github-pr, gitlab-mr, fix-verification.');
640
1231
  }
641
1232
  const writes = renderNodeWritesPath(nodeId, node, context);
642
1233
  if (writes) {
@@ -724,9 +1315,84 @@ function resolvePostTarget(nodeId, node, context, executionContext, source) {
724
1315
  changedFiles: sourceTarget.changedFiles,
725
1316
  };
726
1317
  }
727
- function isPullRequest(value) {
728
- if (!value || typeof value !== 'object') {
729
- return false;
1318
+ async function runCreateChangeRequestWorkflowNode(nodeId, node, context, executionContext) {
1319
+ const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
1320
+ const source = isReviewSource(context.artifacts[sourceArtifact])
1321
+ ? context.artifacts[sourceArtifact]
1322
+ : undefined;
1323
+ const sourceTarget = readSourcePostTarget(source);
1324
+ const aliasPlatform = node.action === 'create-pr' ? 'github' : node.action === 'create-mr' ? 'gitlab' : undefined;
1325
+ const explicitPlatform = getStringActionOption(node, 'platform', context);
1326
+ const platform = aliasPlatform ?? explicitPlatform ?? sourceTarget.platform;
1327
+ if (!isWorkflowPlatform(platform)) {
1328
+ throw new Error(`Workflow change-request node "${nodeId}" must resolve with.platform to github or gitlab.`);
1329
+ }
1330
+ const projectId = resolvePostProjectId(nodeId, node, context, sourceTarget);
1331
+ const configuredSourceBranch = getStringActionOption(node, 'sourceBranch', context)?.trim();
1332
+ const configuredHead = getStringActionOption(node, 'head', context)?.trim();
1333
+ const sourceBranch = configuredSourceBranch && configuredSourceBranch.length > 0
1334
+ ? configuredSourceBranch
1335
+ : configuredHead;
1336
+ const configuredTargetBranch = getStringActionOption(node, 'targetBranch', context)?.trim();
1337
+ const configuredBase = getStringActionOption(node, 'base', context)?.trim();
1338
+ const targetBranch = configuredTargetBranch && configuredTargetBranch.length > 0
1339
+ ? configuredTargetBranch
1340
+ : configuredBase;
1341
+ if (!sourceBranch) {
1342
+ throw new Error(`Workflow change-request node "${nodeId}" must define with.sourceBranch.`);
1343
+ }
1344
+ if (!targetBranch) {
1345
+ throw new Error(`Workflow change-request node "${nodeId}" must define with.targetBranch.`);
1346
+ }
1347
+ const title = requireStringActionOption(nodeId, node, 'title', context);
1348
+ const body = getStringActionOption(node, 'body', context);
1349
+ const draft = getBooleanActionOption(node, 'draft', context);
1350
+ const reuseExisting = !hasActionOption(node, 'reuseExisting') ||
1351
+ getBooleanActionOption(node, 'reuseExisting', context);
1352
+ const platformClient = getWorkflowPlatformClient(executionContext, platform);
1353
+ const input = {
1354
+ sourceBranch,
1355
+ targetBranch,
1356
+ title,
1357
+ body,
1358
+ draft,
1359
+ };
1360
+ const existing = reuseExisting
1361
+ ? await platformClient.findChangeRequest?.(projectId, sourceBranch, targetBranch)
1362
+ : undefined;
1363
+ let operation = existing ? 'reused' : 'created';
1364
+ const changeRequest = existing ??
1365
+ (await platformClient.createChangeRequest(projectId, input).catch(async (error) => {
1366
+ if (reuseExisting) {
1367
+ try {
1368
+ const retryExisting = await platformClient.findChangeRequest?.(projectId, sourceBranch, targetBranch);
1369
+ if (retryExisting) {
1370
+ operation = 'reused';
1371
+ return retryExisting;
1372
+ }
1373
+ }
1374
+ catch {
1375
+ // ignore retry failure; fall through to throw original error
1376
+ }
1377
+ }
1378
+ throw error;
1379
+ }));
1380
+ return {
1381
+ id: nodeId,
1382
+ type: 'action',
1383
+ action: node.action,
1384
+ response: `${operation} ${platform} change request #${changeRequest.number}`,
1385
+ output: {
1386
+ platform,
1387
+ projectId,
1388
+ operation,
1389
+ ...changeRequest,
1390
+ },
1391
+ };
1392
+ }
1393
+ function isPullRequest(value) {
1394
+ if (!value || typeof value !== 'object') {
1395
+ return false;
730
1396
  }
731
1397
  const candidate = value;
732
1398
  return (typeof candidate.number === 'number' &&
@@ -755,19 +1421,24 @@ function createWorkflowLineValidator(platform, source) {
755
1421
  : [];
756
1422
  const patchSources = fileChanges.length > 0 ? fileChanges : (source.filesWithDiffs ?? []);
757
1423
  const validLinesMap = new Map();
1424
+ const changedLinesMap = new Map();
758
1425
  for (const file of patchSources) {
759
1426
  if ('status' in file && file.status === 'removed') {
760
1427
  continue;
761
1428
  }
762
1429
  const patch = file.patch;
763
1430
  if (patch) {
764
- validLinesMap.set(file.filename, parseValidLinesFromPatch(patch));
1431
+ const lineInfo = parseDiffLineInfo(patch);
1432
+ validLinesMap.set(file.filename, lineInfo.commentableLines);
1433
+ changedLinesMap.set(file.filename, lineInfo.addedLines);
765
1434
  }
766
1435
  }
767
1436
  return {
768
1437
  isValidLine(file, line) {
769
- const validLines = validLinesMap.get(file);
770
- return validLines !== undefined && validLines.has(line);
1438
+ return validLinesMap.get(file)?.has(line) ?? false;
1439
+ },
1440
+ isChangedLine(file, line) {
1441
+ return changedLinesMap.get(file)?.has(line) ?? false;
771
1442
  },
772
1443
  };
773
1444
  }
@@ -896,7 +1567,8 @@ async function runDescribeWorkflowNode(config, nodeId, node, options, workingDir
896
1567
  if (!target.pullRequest) {
897
1568
  throw new Error(`Workflow describe node "${nodeId}" needs a platform change-source target.`);
898
1569
  }
899
- const shouldPostDescription = getBooleanActionOption(node, 'post') || getBooleanActionOption(node, 'postDescription');
1570
+ const shouldPostDescription = getBooleanActionOption(node, 'post', context) ||
1571
+ getBooleanActionOption(node, 'postDescription', context);
900
1572
  const runtimeClient = await connectToRuntime(config, source.workingDir ?? workingDir, {
901
1573
  debug: options.debug,
902
1574
  modelOverrides: getDescriberModelOverride(config),
@@ -938,7 +1610,10 @@ async function runPostCommentWorkflowNode(nodeId, node, options, workingDir, con
938
1610
  const rawBody = node.input === undefined
939
1611
  ? requireStringActionOption(nodeId, node, 'body', context)
940
1612
  : renderTemplate(node.input, context);
941
- const marker = getStringActionOption(node, 'marker', context)?.trim();
1613
+ const configuredMarker = getStringActionOption(node, 'marker', context)?.trim();
1614
+ const marker = configuredMarker && configuredMarker.length > 0
1615
+ ? configuredMarker
1616
+ : options.idempotencyContext?.idempotencyKey;
942
1617
  const body = formatMarkedComment(rawBody, marker);
943
1618
  let operation = 'created';
944
1619
  if (marker) {
@@ -988,7 +1663,7 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
988
1663
  ? createWorkflowInlinePosition(target.platform, source)
989
1664
  : undefined;
990
1665
  const shouldRemoveErrorComment = !hasActionOption(node, 'removeErrorComment') ||
991
- getBooleanActionOption(node, 'removeErrorComment');
1666
+ getBooleanActionOption(node, 'removeErrorComment', context);
992
1667
  await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
993
1668
  if (shouldRemoveErrorComment) {
994
1669
  await removeErrorComment(target.platformClient, target.projectId, target.prNumber);
@@ -1015,6 +1690,256 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
1015
1690
  },
1016
1691
  };
1017
1692
  }
1693
+ function getFixStatusDisposition(finding) {
1694
+ const disposition = finding.disposition;
1695
+ if (disposition === 'resolved')
1696
+ return 'resolved';
1697
+ if (disposition === 'partial')
1698
+ return 'partial';
1699
+ if (disposition === 'regression')
1700
+ return 'regression';
1701
+ return 'still-open';
1702
+ }
1703
+ function getVerificationDisposition(finding, verdict, thresholdRank) {
1704
+ if (severityRank(finding.issue.severity) < thresholdRank) {
1705
+ return finding.disposition;
1706
+ }
1707
+ if (!verdict) {
1708
+ if (finding.disposition === 'regression') {
1709
+ return 'regression';
1710
+ }
1711
+ return 'still_open';
1712
+ }
1713
+ return verdict.disposition === 'still_open' ? 'still_open' : verdict.disposition;
1714
+ }
1715
+ function reconcileReviewArtifactFindings(artifact, reReview, options) {
1716
+ const now = new Date().toISOString();
1717
+ const thresholdRank = severityRank(options.severity);
1718
+ const verdicts = new Map((reReview.verification?.findings ?? []).map((finding) => [finding.id, finding]));
1719
+ const existingFingerprints = new Set(artifact.findings.map((finding) => finding.fingerprint));
1720
+ const reconciledFindings = artifact.findings.map((finding) => {
1721
+ const verdict = verdicts.get(finding.id);
1722
+ const disposition = getVerificationDisposition(finding, verdict, thresholdRank);
1723
+ const issue = isReviewIssue(verdict?.issue) ? verdict.issue : finding.issue;
1724
+ const fingerprint = issue === finding.issue ? finding.fingerprint : createIssueFingerprint(issue);
1725
+ const shouldVerify = severityRank(finding.issue.severity) >= thresholdRank;
1726
+ const verification = shouldVerify
1727
+ ? {
1728
+ disposition: verdict?.disposition ?? 'missing',
1729
+ rationale: verdict?.rationale,
1730
+ verifiedAt: now,
1731
+ }
1732
+ : finding.verification;
1733
+ return {
1734
+ ...finding,
1735
+ issue,
1736
+ fingerprint,
1737
+ state: disposition === 'resolved' ? 'resolved' : 'open',
1738
+ disposition,
1739
+ verification,
1740
+ updatedAt: now,
1741
+ };
1742
+ });
1743
+ for (const issue of reReview.issues) {
1744
+ const fingerprint = createIssueFingerprint(issue);
1745
+ if (existingFingerprints.has(fingerprint)) {
1746
+ continue;
1747
+ }
1748
+ existingFingerprints.add(fingerprint);
1749
+ reconciledFindings.push({
1750
+ id: `R${reconciledFindings.length + 1}`,
1751
+ fingerprint,
1752
+ issue,
1753
+ state: 'open',
1754
+ disposition: 'regression',
1755
+ verification: undefined,
1756
+ source: 'agent',
1757
+ createdAt: now,
1758
+ updatedAt: now,
1759
+ });
1760
+ }
1761
+ const updatedArtifact = { ...artifact, findings: reconciledFindings };
1762
+ const statuses = updatedArtifact.findings.map((finding) => {
1763
+ const disposition = getFixStatusDisposition(finding);
1764
+ const verificationMissing = severityRank(finding.issue.severity) >= thresholdRank && !verdicts.has(finding.id);
1765
+ const diffSnippet = disposition === 'resolved' || disposition === 'regression'
1766
+ ? extractDiffSnippet(options.fixSource, finding.issue.file, finding.issue.line)
1767
+ : undefined;
1768
+ return { finding, disposition, diffSnippet, verificationMissing };
1769
+ });
1770
+ const actionableOpen = updatedArtifact.findings.filter((finding) => finding.state === 'open' &&
1771
+ severityRank(finding.issue.severity) >= thresholdRank &&
1772
+ (finding.disposition === 'still_open' ||
1773
+ finding.disposition === 'partial' ||
1774
+ finding.disposition === 'regression')).length;
1775
+ return {
1776
+ artifact: updatedArtifact,
1777
+ statuses,
1778
+ shouldContinue: actionableOpen >= options.minIssues,
1779
+ actionableOpen,
1780
+ resolved: updatedArtifact.findings.filter((finding) => finding.state === 'resolved').length,
1781
+ partial: updatedArtifact.findings.filter((finding) => finding.disposition === 'partial').length,
1782
+ stillOpen: updatedArtifact.findings.filter((finding) => finding.disposition === 'still_open')
1783
+ .length,
1784
+ regression: updatedArtifact.findings.filter((finding) => finding.disposition === 'regression')
1785
+ .length,
1786
+ };
1787
+ }
1788
+ function extractDiffSnippet(fixChange, filePath, line) {
1789
+ if (!fixChange?.filesWithDiffs) {
1790
+ return undefined;
1791
+ }
1792
+ const fileWithDiff = fixChange.filesWithDiffs.find((f) => f.filename === filePath);
1793
+ if (!fileWithDiff?.patch) {
1794
+ return undefined;
1795
+ }
1796
+ const lines = fileWithDiff.patch.split('\n');
1797
+ if (!line) {
1798
+ return lines.slice(0, 15).join('\n');
1799
+ }
1800
+ for (let i = 0; i < lines.length; i++) {
1801
+ const hunkMatch = lines[i]?.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
1802
+ if (!hunkMatch) {
1803
+ continue;
1804
+ }
1805
+ const newStart = Number.parseInt(hunkMatch[1], 10);
1806
+ const newLineCount = hunkMatch[2] ? Number.parseInt(hunkMatch[2], 10) : 1;
1807
+ if (line < newStart || line >= newStart + newLineCount) {
1808
+ continue;
1809
+ }
1810
+ let end = i + 1;
1811
+ while (end < lines.length && !lines[end]?.startsWith('@@')) {
1812
+ end++;
1813
+ }
1814
+ return lines.slice(i, end).join('\n');
1815
+ }
1816
+ return lines.slice(0, 15).join('\n');
1817
+ }
1818
+ function formatFixStatusComment(statuses, stackedPrUrl) {
1819
+ const lines = ['## Fix Status', ''];
1820
+ if (statuses.length === 0) {
1821
+ lines.push('No findings to report.');
1822
+ return lines.join('\n');
1823
+ }
1824
+ lines.push('| # | Severity | File | Issue | Status |');
1825
+ lines.push('|---|----------|------|-------|--------|');
1826
+ for (let i = 0; i < statuses.length; i++) {
1827
+ const s = statuses[i];
1828
+ const statusIcon = s.disposition === 'resolved'
1829
+ ? '✅ Resolved'
1830
+ : s.disposition === 'partial'
1831
+ ? '🟡 Partial'
1832
+ : s.disposition === 'regression'
1833
+ ? '🔴 Regression'
1834
+ : s.disposition === 'attempted'
1835
+ ? '🔧 Attempted'
1836
+ : s.verificationMissing
1837
+ ? '⚠️ Verification Missing'
1838
+ : '⚪ Still Open';
1839
+ const file = `${s.finding.issue.file}${s.finding.issue.line ? `:${s.finding.issue.line}` : ''}`;
1840
+ lines.push(`| ${i + 1} | ${s.finding.issue.severity} | ${file} | ${s.finding.issue.title} | ${statusIcon} |`);
1841
+ }
1842
+ const resolved = statuses.filter((s) => s.disposition === 'resolved' && s.diffSnippet);
1843
+ if (resolved.length > 0) {
1844
+ lines.push('');
1845
+ lines.push('### Fix Details');
1846
+ for (let i = 0; i < statuses.length; i++) {
1847
+ const s = statuses[i];
1848
+ if (s.disposition === 'resolved' && s.diffSnippet) {
1849
+ lines.push('');
1850
+ lines.push(`**#${i + 1} — ${s.finding.issue.title} (${s.disposition})**`);
1851
+ lines.push('```diff');
1852
+ lines.push(s.diffSnippet);
1853
+ lines.push('```');
1854
+ }
1855
+ }
1856
+ }
1857
+ if (stackedPrUrl) {
1858
+ lines.push('');
1859
+ lines.push(`Stacked fix PR: ${stackedPrUrl}`);
1860
+ }
1861
+ return lines.join('\n');
1862
+ }
1863
+ async function runPostFixStatusWorkflowNode(nodeId, node, options, workingDir, context, executionContext) {
1864
+ const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
1865
+ const source = isReviewSource(context.artifacts[sourceArtifact])
1866
+ ? context.artifacts[sourceArtifact]
1867
+ : undefined;
1868
+ const target = resolvePostTarget(nodeId, node, context, executionContext, source);
1869
+ const reviewArtifactName = getStringActionOption(node, 'reviewArtifact', context) ?? 'reviewArtifact';
1870
+ const reviewArtifactValue = context.artifacts[reviewArtifactName];
1871
+ let artifactPayload;
1872
+ if (isReviewArtifactPayload(reviewArtifactValue)) {
1873
+ artifactPayload = reviewArtifactValue;
1874
+ }
1875
+ else if (reviewArtifactValue &&
1876
+ typeof reviewArtifactValue === 'object' &&
1877
+ 'payload' in reviewArtifactValue) {
1878
+ const payload = reviewArtifactValue.payload;
1879
+ if (isReviewArtifactPayload(payload)) {
1880
+ artifactPayload = payload;
1881
+ }
1882
+ }
1883
+ if (!artifactPayload) {
1884
+ throw new Error(`Workflow post-fix-status node "${nodeId}" needs a review artifact (with.reviewArtifact).`);
1885
+ }
1886
+ const fixReviewName = getStringActionOption(node, 'fixReview', context);
1887
+ const fixReviewResult = fixReviewName ? context.artifacts[fixReviewName] : undefined;
1888
+ const hasReReview = isReviewResult(fixReviewResult);
1889
+ const fixChangeName = getStringActionOption(node, 'fixChange', context);
1890
+ const fixChange = fixChangeName ? context.artifacts[fixChangeName] : undefined;
1891
+ const fixSource = isReviewSource(fixChange) ? fixChange : undefined;
1892
+ const stackedPrUrl = getStringActionOption(node, 'stackedPrUrl', context);
1893
+ const marker = getStringActionOption(node, 'marker', context)?.trim() ?? 'drs-fix-status';
1894
+ const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
1895
+ const thresholdRank = severityRank(severity);
1896
+ const originalFindings = artifactPayload.findings.filter((finding) => severityRank(finding.issue.severity) >= thresholdRank);
1897
+ const statuses = originalFindings.map((finding) => {
1898
+ if (hasReReview) {
1899
+ const disposition = getFixStatusDisposition(finding);
1900
+ const diffSnippet = disposition === 'resolved' || disposition === 'regression'
1901
+ ? extractDiffSnippet(fixSource, finding.issue.file, finding.issue.line)
1902
+ : undefined;
1903
+ return { finding, disposition, diffSnippet };
1904
+ }
1905
+ const diffSnippet = extractDiffSnippet(fixSource, finding.issue.file, finding.issue.line);
1906
+ return { finding, disposition: 'attempted', diffSnippet };
1907
+ });
1908
+ const body = formatFixStatusComment(statuses, stackedPrUrl);
1909
+ const markedBody = formatMarkedComment(body, marker);
1910
+ let operation = 'created';
1911
+ try {
1912
+ const comments = await target.platformClient.getComments(target.projectId, target.prNumber);
1913
+ const existingComment = findExistingCommentById(comments, marker);
1914
+ if (existingComment) {
1915
+ await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.updateComment(target.projectId, target.prNumber, existingComment.id, markedBody));
1916
+ operation = 'updated';
1917
+ }
1918
+ else {
1919
+ await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.createComment(target.projectId, target.prNumber, markedBody));
1920
+ }
1921
+ }
1922
+ catch (error) {
1923
+ throw new Error(`Workflow post-fix-status node "${nodeId}" failed to post comment: ${error instanceof Error ? error.message : String(error)}`);
1924
+ }
1925
+ return {
1926
+ id: nodeId,
1927
+ type: 'action',
1928
+ action: node.action,
1929
+ response: `${operation} fix-status comment on ${target.platform} ${target.projectId}#${target.prNumber}`,
1930
+ output: {
1931
+ platform: target.platform,
1932
+ projectId: target.projectId,
1933
+ prNumber: target.prNumber,
1934
+ operation,
1935
+ resolved: statuses.filter((s) => s.disposition === 'resolved').length,
1936
+ partial: statuses.filter((s) => s.disposition === 'partial').length,
1937
+ stillOpen: statuses.filter((s) => s.disposition === 'still-open').length,
1938
+ regression: statuses.filter((s) => s.disposition === 'regression').length,
1939
+ attempted: statuses.filter((s) => s.disposition === 'attempted').length,
1940
+ },
1941
+ };
1942
+ }
1018
1943
  function isReviewSource(value) {
1019
1944
  if (!value || typeof value !== 'object') {
1020
1945
  return false;
@@ -1025,23 +1950,6 @@ function isReviewSource(value) {
1025
1950
  typeof candidate.context === 'object' &&
1026
1951
  candidate.context !== null);
1027
1952
  }
1028
- async function enforceWorkflowReviewTarget(config, source, workingDir) {
1029
- const platform = typeof source.context.platform === 'string' ? source.context.platform : undefined;
1030
- if (!isWorkflowPlatform(platform)) {
1031
- return;
1032
- }
1033
- const projectId = typeof source.context.projectId === 'string' ? source.context.projectId : undefined;
1034
- const pullRequest = isPullRequest(source.context.pullRequest)
1035
- ? source.context.pullRequest
1036
- : undefined;
1037
- if (!projectId || !pullRequest) {
1038
- throw new Error('Workflow platform review source is missing project or PR/MR metadata.');
1039
- }
1040
- await enforceRepoBranchMatch(workingDir, projectId, pullRequest, {
1041
- skipRepoCheck: config.review.skipRepoCheck,
1042
- skipBranchCheck: config.review.skipBranchCheck,
1043
- });
1044
- }
1045
1953
  async function runReviewWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
1046
1954
  const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
1047
1955
  const source = context.artifacts[sourceArtifact];
@@ -1049,10 +1957,46 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
1049
1957
  throw new Error(`Workflow review node "${nodeId}" needs a ReviewSource artifact. ` +
1050
1958
  'Set with.source to a change-source output.');
1051
1959
  }
1960
+ const reviewArtifactName = getStringActionOption(node, 'reviewArtifact', context);
1961
+ const reviewArtifactEnvelope = reviewArtifactName
1962
+ ? context.artifacts[reviewArtifactName]
1963
+ : undefined;
1964
+ const reviewArtifact = reviewArtifactName
1965
+ ? getReviewArtifactPayloadFromValue(nodeId, reviewArtifactEnvelope)
1966
+ : undefined;
1967
+ const reviewArtifactPath = reviewArtifactEnvelope && typeof reviewArtifactEnvelope === 'object'
1968
+ ? reviewArtifactEnvelope.path
1969
+ : undefined;
1970
+ const severity = getStringActionOption(node, 'severity', context)?.toUpperCase();
1971
+ const traceCollector = executionContext.traceCollector;
1972
+ const sourceForReview = reviewArtifact
1973
+ ? {
1974
+ ...source,
1975
+ context: {
1976
+ ...source.context,
1977
+ verification: {
1978
+ artifact: {
1979
+ reviewId: reviewArtifact.reviewId,
1980
+ findings: reviewArtifact.findings,
1981
+ },
1982
+ artifactPath: reviewArtifactPath,
1983
+ severity,
1984
+ },
1985
+ traceCollector,
1986
+ },
1987
+ }
1988
+ : {
1989
+ ...source,
1990
+ context: {
1991
+ ...source.context,
1992
+ traceCollector,
1993
+ },
1994
+ };
1995
+ if (traceCollector) {
1996
+ const agentIds = getReviewAgentIds(config);
1997
+ traceCollector.setContext(nodeId, agentIds[0] ?? 'review/unified-reviewer', '');
1998
+ }
1052
1999
  const reviewResult = await withWorkflowLock(executionContext.locks.exit, async () => {
1053
- const restoreExit = setExitHandler((code) => {
1054
- throw new ExitError(code);
1055
- });
1056
2000
  const originalLog = console.log;
1057
2001
  const originalWarn = console.warn;
1058
2002
  if (options.jsonOutput) {
@@ -1060,38 +2004,51 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
1060
2004
  console.warn = () => undefined;
1061
2005
  }
1062
2006
  try {
1063
- await enforceWorkflowReviewTarget(config, source, source.workingDir ?? workingDir);
1064
2007
  return await executeReview(config, {
1065
- ...source,
1066
- workingDir: source.workingDir ?? workingDir,
2008
+ ...sourceForReview,
2009
+ workingDir: sourceForReview.workingDir ?? workingDir,
1067
2010
  debug: options.debug,
1068
2011
  thinkingLevel: options.thinkingLevel,
1069
2012
  });
1070
2013
  }
1071
- catch (error) {
1072
- if (error instanceof ExitError) {
1073
- throw new Error(`Workflow review node "${nodeId}" failed: all review agents failed.`);
1074
- }
1075
- throw error;
1076
- }
1077
2014
  finally {
1078
2015
  if (options.jsonOutput) {
1079
2016
  console.log = originalLog;
1080
2017
  console.warn = originalWarn;
1081
2018
  }
1082
- restoreExit();
1083
2019
  }
1084
2020
  });
1085
2021
  const writes = renderNodeWritesPath(nodeId, node, context);
1086
2022
  if (writes) {
1087
2023
  await writeWorkflowFile(workingDir, writes, JSON.stringify(reviewResult, null, 2));
1088
2024
  }
2025
+ const explicitArtifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
2026
+ let artifactOutput;
2027
+ if (explicitArtifactOutput) {
2028
+ artifactOutput = explicitArtifactOutput;
2029
+ }
2030
+ else if (!reviewArtifactName) {
2031
+ artifactOutput = `${nodeId}Artifact`;
2032
+ }
2033
+ const outputs = {};
2034
+ const reviewArtifactPayload = createReviewArtifactPayload(reviewResult, source);
2035
+ const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
2036
+ const saved = await saveWorkflowArtifact(workingDir, {
2037
+ kind: 'review',
2038
+ scope,
2039
+ payload: reviewArtifactPayload,
2040
+ });
2041
+ const artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
2042
+ if (artifactOutput) {
2043
+ outputs[artifactOutput] = { ...saved.artifact, path: saved.path, latestPath: saved.latestPath };
2044
+ }
1089
2045
  return {
1090
2046
  id: nodeId,
1091
2047
  type: 'action',
1092
2048
  action: node.action,
1093
- response: JSON.stringify(reviewResult.summary, null, 2),
2049
+ response: `${JSON.stringify(reviewResult.summary, null, 2)}${artifactResponse}`,
1094
2050
  output: reviewResult,
2051
+ outputs,
1095
2052
  writes,
1096
2053
  };
1097
2054
  }
@@ -1101,70 +2058,331 @@ function recordNodeArtifact(nodeId, node, result, artifacts) {
1101
2058
  if (node.output) {
1102
2059
  artifacts[node.output] = artifactValue;
1103
2060
  }
2061
+ if (result.outputs) {
2062
+ for (const [name, value] of Object.entries(result.outputs)) {
2063
+ artifacts[name] = value;
2064
+ }
2065
+ }
2066
+ }
2067
+ function logWorkflowNodeRunning(nodeId, options) {
2068
+ if (!options.jsonOutput) {
2069
+ console.log(chalk.gray(`Running node ${nodeId}...`));
2070
+ }
2071
+ }
2072
+ function logWorkflowNodeSkipped(nodeId, reason, options) {
2073
+ if (!options.jsonOutput) {
2074
+ console.log(chalk.gray(`Skipping node ${nodeId} (${reason})`));
2075
+ }
2076
+ }
2077
+ function recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts) {
2078
+ result.status ??= 'success';
2079
+ nodes[nodeId] = result;
2080
+ if (result.status !== 'skipped') {
2081
+ recordNodeArtifact(nodeId, node, result, artifacts);
2082
+ }
2083
+ }
2084
+ function completeWorkflowNodeResult(result, startedAt) {
2085
+ const completedAt = new Date().toISOString();
2086
+ result.startedAt ??= startedAt;
2087
+ result.completedAt ??= completedAt;
2088
+ result.durationMs ??= Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime());
2089
+ return result;
2090
+ }
2091
+ async function runWorkflowDagSegment(workflowNodes, nodeIds, activeNodeIds, options, context, nodeExecutor) {
2092
+ const completed = new Set();
2093
+ const segmentNodeIds = new Set(nodeIds);
2094
+ if (activeNodeIds) {
2095
+ for (const nodeId of nodeIds) {
2096
+ if (!activeNodeIds.has(nodeId)) {
2097
+ completed.add(nodeId);
2098
+ if (context.nodes[nodeId] === undefined) {
2099
+ logWorkflowNodeSkipped(nodeId, 'inactive branch', options);
2100
+ context.nodes[nodeId] = createSkippedWorkflowNodeResult(nodeId);
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+ while (completed.size < nodeIds.length) {
2106
+ const runnable = nodeIds.filter((nodeId) => {
2107
+ if (completed.has(nodeId))
2108
+ return false;
2109
+ const node = workflowNodes[nodeId];
2110
+ if (!node)
2111
+ return false;
2112
+ return getNodeNeeds(node).every((dependency) => completed.has(dependency) || !segmentNodeIds.has(dependency));
2113
+ });
2114
+ if (runnable.length === 0) {
2115
+ throw new Error('Workflow control runner could not make progress in a DAG segment.');
2116
+ }
2117
+ const settled = await Promise.allSettled(runnable.map(async (nodeId) => {
2118
+ const node = workflowNodes[nodeId];
2119
+ if (!node) {
2120
+ throw new Error(`Workflow references unknown node "${nodeId}".`);
2121
+ }
2122
+ const startedAt = new Date().toISOString();
2123
+ const skipReason = getWorkflowNodeSkipReason(node, context);
2124
+ if (skipReason) {
2125
+ logWorkflowNodeSkipped(nodeId, skipReason, options);
2126
+ return {
2127
+ nodeId,
2128
+ node,
2129
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2130
+ };
2131
+ }
2132
+ logWorkflowNodeRunning(nodeId, options);
2133
+ const result = await nodeExecutor.runNode(nodeId, node, context);
2134
+ return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
2135
+ }));
2136
+ const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
2137
+ if (firstRejection) {
2138
+ throw firstRejection.reason instanceof Error
2139
+ ? firstRejection.reason
2140
+ : new Error(String(firstRejection.reason));
2141
+ }
2142
+ for (const outcome of settled) {
2143
+ const { nodeId, node, result } = outcome.value;
2144
+ completed.add(nodeId);
2145
+ recordWorkflowNodeResult(nodeId, node, result, context.nodes, context.artifacts);
2146
+ }
2147
+ }
2148
+ }
2149
+ async function runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor) {
2150
+ await runControlWorkflowFromSegments(workflowNodes, splitWorkflowSegments(workflowNodes, executionOrder), options, context, nodeExecutor);
2151
+ }
2152
+ async function runControlWorkflowFromSegments(workflowNodes, compiledSegments, options, context, nodeExecutor) {
2153
+ const segments = compiledSegments.map((segment) => segment.type === 'control'
2154
+ ? { type: 'control', nodeId: segment.nodeId }
2155
+ : {
2156
+ type: 'dag',
2157
+ nodeIds: [...segment.nodeIds],
2158
+ ...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
2159
+ });
2160
+ let segmentIndex = 0;
2161
+ while (segmentIndex < segments.length) {
2162
+ const segment = segments[segmentIndex];
2163
+ if (segment.type === 'dag') {
2164
+ await runWorkflowDagSegment(workflowNodes, segment.nodeIds, segment.activeNodeIds, options, context, nodeExecutor);
2165
+ segmentIndex += 1;
2166
+ continue;
2167
+ }
2168
+ const node = workflowNodes[segment.nodeId];
2169
+ if (!node) {
2170
+ throw new Error(`Workflow references unknown node "${segment.nodeId}".`);
2171
+ }
2172
+ const skipReason = getWorkflowNodeSkipReason(node, context);
2173
+ if (skipReason) {
2174
+ logWorkflowNodeSkipped(segment.nodeId, skipReason, options);
2175
+ }
2176
+ else {
2177
+ logWorkflowNodeRunning(segment.nodeId, options);
2178
+ }
2179
+ const startedAt = new Date().toISOString();
2180
+ const controlResult = skipReason
2181
+ ? { result: createSkippedWorkflowNodeResult(segment.nodeId) }
2182
+ : runControlWorkflowNode(segment.nodeId, node, context);
2183
+ const { result, nextNodeId, ended } = {
2184
+ ...controlResult,
2185
+ result: completeWorkflowNodeResult(controlResult.result, startedAt),
2186
+ };
2187
+ recordWorkflowNodeResult(segment.nodeId, node, result, context.nodes, context.artifacts);
2188
+ if (ended) {
2189
+ return;
2190
+ }
2191
+ if (!nextNodeId) {
2192
+ segmentIndex += 1;
2193
+ continue;
2194
+ }
2195
+ const targetIndex = findWorkflowSegmentIndex(segments, nextNodeId);
2196
+ if (targetIndex < 0) {
2197
+ throw new Error(`Workflow control node "${segment.nodeId}" targets unknown node "${nextNodeId}".`);
2198
+ }
2199
+ if (node.control !== 'loop' && targetIndex <= segmentIndex) {
2200
+ throw new Error(`Workflow control node "${segment.nodeId}" cannot jump backward to "${nextNodeId}". Use control: loop with maxIterations for repeated execution.`);
2201
+ }
2202
+ const targetSegment = segments[targetIndex];
2203
+ if (targetSegment.type === 'dag') {
2204
+ targetSegment.activeNodeIds = computeActiveWorkflowNodes(workflowNodes, targetSegment.nodeIds, nextNodeId, !(node.control === 'loop' && nextNodeId === node.target));
2205
+ }
2206
+ segmentIndex = targetIndex;
2207
+ }
1104
2208
  }
1105
2209
  function formatWorkflowJson(result) {
1106
2210
  return JSON.stringify(result, null, 2);
1107
2211
  }
1108
2212
  async function runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
1109
2213
  const kind = getNodeKind(node);
2214
+ if (getWorkflowNodeSkipReason(node, context)) {
2215
+ return createSkippedWorkflowNodeResult(nodeId);
2216
+ }
1110
2217
  if (kind === 'agent') {
1111
- return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context);
2218
+ return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
1112
2219
  }
1113
2220
  if (kind === 'agents') {
1114
2221
  return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
1115
2222
  }
2223
+ if (kind === 'control') {
2224
+ throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
2225
+ }
1116
2226
  return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
1117
2227
  }
2228
+ /**
2229
+ * In-process {@link NodeExecutor} that dispatches a single non-control node
2230
+ * to the existing side-effecting runner functions. Constructed once per
2231
+ * workflow run so git/platform clients and locks are shared across nodes.
2232
+ *
2233
+ * The Temporal executor will provide its own NodeExecutor whose `runNode`
2234
+ * schedules a Temporal activity instead of executing in-process.
2235
+ */
2236
+ class LocalNodeExecutor {
2237
+ config;
2238
+ options;
2239
+ workingDir;
2240
+ executionContext;
2241
+ constructor(config, options, workingDir, executionContext) {
2242
+ this.config = config;
2243
+ this.options = options;
2244
+ this.workingDir = workingDir;
2245
+ this.executionContext = executionContext;
2246
+ }
2247
+ async runNode(nodeId, node, context) {
2248
+ return runSingleWorkflowNode(this.config, nodeId, node, this.options, this.workingDir, context, this.executionContext);
2249
+ }
2250
+ }
2251
+ export async function runWorkflowNodeLocally(config, nodeId, node, options, workingDir, context) {
2252
+ const executionContext = {
2253
+ gitClients: new Map(),
2254
+ platformClients: {},
2255
+ traceCollector: options.trace ? new TraceCollector() : undefined,
2256
+ locks: {
2257
+ exit: createWorkflowLock(),
2258
+ console: createWorkflowLock(),
2259
+ },
2260
+ };
2261
+ return runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2262
+ }
1118
2263
  export async function runWorkflow(config, workflowName, options = {}) {
1119
2264
  const workflow = config.workflows?.[workflowName];
1120
2265
  if (!workflow) {
1121
2266
  throw new Error(`Unknown workflow "${workflowName}".`);
1122
2267
  }
1123
2268
  const workflowNodes = getWorkflowNodes(workflowName, workflow);
2269
+ const executionOrder = getWorkflowExecutionOrder(workflowNodes);
2270
+ const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
2271
+ return executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflow.output, hasWorkflowControlNodes(workflowNodes), options);
2272
+ }
2273
+ /**
2274
+ * Run a workflow from a previously compiled {@link CompiledWorkflowPlan}.
2275
+ *
2276
+ * The plan supplies the scheduling data (nodes, execution order, waves,
2277
+ * segments, output key) so the caller does not need to reload repo config to
2278
+ * drive execution. `config` is still required because agent/action node
2279
+ * execution needs agent and review configuration that lives outside the
2280
+ * compiled plan.
2281
+ */
2282
+ export async function runWorkflowFromCompiledPlan(config, plan, options = {}) {
2283
+ const executionSegments = plan.hasControlNodes ? plan.segments : [];
2284
+ if (plan.hasControlNodes) {
2285
+ const workflowNodes = plan.nodes;
2286
+ const order = plan.executionOrder;
2287
+ return executeWorkflowRun(config, plan.workflowName, workflowNodes, order, plan.waves, plan.output, true, options, executionSegments);
2288
+ }
2289
+ return executeWorkflowRun(config, plan.workflowName, plan.nodes, plan.executionOrder, plan.waves, plan.output, false, options);
2290
+ }
2291
+ async function executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflowOutput, hasControl, options, controlSegments) {
2292
+ const workflow = config.workflows?.[workflowName];
2293
+ if (!workflow) {
2294
+ throw new Error(`Unknown workflow "${workflowName}".`);
2295
+ }
1124
2296
  const workingDir = options.workingDir ?? process.cwd();
1125
2297
  const inputs = await resolveWorkflowInputs(workflow, options, workingDir);
1126
2298
  const nodes = {};
1127
2299
  const artifacts = {};
1128
- const context = { inputs, nodes, artifacts };
2300
+ const loop = {};
2301
+ const context = { inputs, nodes, artifacts, loop };
1129
2302
  const executionContext = {
1130
2303
  gitClients: new Map(),
1131
2304
  platformClients: {},
2305
+ traceCollector: options.trace ? new TraceCollector() : undefined,
1132
2306
  locks: {
1133
2307
  exit: createWorkflowLock(),
1134
2308
  console: createWorkflowLock(),
1135
2309
  },
1136
2310
  };
1137
- const executionOrder = getWorkflowExecutionOrder(workflowNodes);
1138
- const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
2311
+ const nodeExecutor = new LocalNodeExecutor(config, options, workingDir, executionContext);
1139
2312
  if (!options.jsonOutput) {
1140
2313
  console.log(chalk.gray(`Running workflow ${workflowName}...\n`));
1141
2314
  }
1142
- for (const wave of executionWaves) {
1143
- const results = await Promise.all(wave.map(async (nodeId) => {
1144
- const node = workflowNodes[nodeId];
1145
- if (!node) {
1146
- throw new Error(`Workflow references unknown node "${nodeId}".`);
2315
+ try {
2316
+ if (hasControl) {
2317
+ if (controlSegments) {
2318
+ const internalSegments = controlSegments.map((segment) => segment.type === 'control'
2319
+ ? { type: 'control', nodeId: segment.nodeId }
2320
+ : {
2321
+ type: 'dag',
2322
+ nodeIds: segment.nodeIds,
2323
+ ...(segment.activeNodeIds ? { activeNodeIds: new Set(segment.activeNodeIds) } : {}),
2324
+ });
2325
+ await runControlWorkflowFromSegments(workflowNodes, internalSegments, options, context, nodeExecutor);
1147
2326
  }
1148
- if (!options.jsonOutput) {
1149
- console.log(chalk.gray(`Running node ${nodeId}...`));
2327
+ else {
2328
+ await runControlWorkflow(workflowNodes, executionOrder, options, context, nodeExecutor);
2329
+ }
2330
+ }
2331
+ else {
2332
+ for (const wave of executionWaves) {
2333
+ const settled = await Promise.allSettled(wave.map(async (nodeId) => {
2334
+ const node = workflowNodes[nodeId];
2335
+ if (!node) {
2336
+ throw new Error(`Workflow references unknown node "${nodeId}".`);
2337
+ }
2338
+ const startedAt = new Date().toISOString();
2339
+ const skipReason = getWorkflowNodeSkipReason(node, context);
2340
+ if (skipReason) {
2341
+ logWorkflowNodeSkipped(nodeId, skipReason, options);
2342
+ return {
2343
+ nodeId,
2344
+ node,
2345
+ result: completeWorkflowNodeResult(createSkippedWorkflowNodeResult(nodeId), startedAt),
2346
+ };
2347
+ }
2348
+ logWorkflowNodeRunning(nodeId, options);
2349
+ const result = await nodeExecutor.runNode(nodeId, node, context);
2350
+ return { nodeId, node, result: completeWorkflowNodeResult(result, startedAt) };
2351
+ }));
2352
+ const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
2353
+ if (firstRejection) {
2354
+ throw firstRejection.reason instanceof Error
2355
+ ? firstRejection.reason
2356
+ : new Error(String(firstRejection.reason));
2357
+ }
2358
+ for (const outcome of settled) {
2359
+ const { nodeId, node, result } = outcome.value;
2360
+ recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts);
2361
+ }
2362
+ }
2363
+ }
2364
+ }
2365
+ catch (error) {
2366
+ if (executionContext.traceCollector && executionContext.traceCollector.getTraces().length > 0) {
2367
+ try {
2368
+ await flushWorkflowTrace(executionContext.traceCollector, workflowName, inputs, new Date().toISOString(), workingDir, options);
2369
+ }
2370
+ catch (flushError) {
2371
+ console.error(chalk.yellow('Warning:'), 'Failed to persist workflow trace on failure:', flushError instanceof Error ? flushError.message : String(flushError));
1150
2372
  }
1151
- const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
1152
- return { nodeId, node, result };
1153
- }));
1154
- for (const { nodeId, node, result } of results) {
1155
- nodes[nodeId] = result;
1156
- recordNodeArtifact(nodeId, node, result, artifacts);
1157
2373
  }
2374
+ throw error;
1158
2375
  }
1159
2376
  const lastNodeId = executionOrder[executionOrder.length - 1];
1160
2377
  const lastNode = workflowNodes[lastNodeId];
1161
- const outputKey = lastNode.output ?? lastNodeId;
2378
+ const outputKey = workflowOutput ?? lastNode.output ?? lastNodeId;
1162
2379
  const result = {
1163
2380
  timestamp: new Date().toISOString(),
1164
2381
  workflow: workflowName,
1165
2382
  inputs,
1166
2383
  nodes,
1167
2384
  artifacts,
2385
+ loop,
1168
2386
  output: artifacts[outputKey],
1169
2387
  };
1170
2388
  if (options.outputPath) {
@@ -1173,6 +2391,9 @@ export async function runWorkflow(config, workflowName, options = {}) {
1173
2391
  console.log(chalk.green(`\n✓ Workflow output saved to ${options.outputPath}`));
1174
2392
  }
1175
2393
  }
2394
+ if (executionContext.traceCollector && executionContext.traceCollector.getTraces().length > 0) {
2395
+ await flushWorkflowTrace(executionContext.traceCollector, workflowName, inputs, result.timestamp, workingDir, options);
2396
+ }
1176
2397
  if (options.jsonOutput) {
1177
2398
  console.log(formatWorkflowJson(result));
1178
2399
  }
@@ -1203,6 +2424,7 @@ export function listWorkflows(config, options = {}) {
1203
2424
  source: info.source,
1204
2425
  overridden: info.overridesPackaged,
1205
2426
  description: workflow.description,
2427
+ metadata: workflow.metadata,
1206
2428
  };
1207
2429
  })
1208
2430
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -1226,4 +2448,214 @@ export function listWorkflows(config, options = {}) {
1226
2448
  }
1227
2449
  return entries;
1228
2450
  }
2451
+ function getWorkflowNodeRoutes(node) {
2452
+ if (node.control === 'loop') {
2453
+ return { target: node.target, exit: node.exit };
2454
+ }
2455
+ if (node.control === 'switch') {
2456
+ return { cases: node.cases, default: node.default };
2457
+ }
2458
+ if (node.control === 'passThrough') {
2459
+ return { target: node.target };
2460
+ }
2461
+ return undefined;
2462
+ }
2463
+ function formatWorkflowInput(input) {
2464
+ if (typeof input === 'string') {
2465
+ return JSON.stringify(input);
2466
+ }
2467
+ if (input.file !== undefined) {
2468
+ return `file:${input.file}`;
2469
+ }
2470
+ return JSON.stringify(input.value ?? '');
2471
+ }
2472
+ function buildWorkflowDetail(name, workflow, workingDir) {
2473
+ const sourceInfo = loadWorkflowSourceInfo(workingDir);
2474
+ const info = sourceInfo[name] ?? {
2475
+ source: 'packaged',
2476
+ overridesPackaged: false,
2477
+ };
2478
+ const workflowNodes = getWorkflowNodes(name, workflow);
2479
+ getWorkflowExecutionOrder(workflowNodes);
2480
+ const graph = buildWorkflowGraph(name, workflow);
2481
+ return {
2482
+ name,
2483
+ source: info.source,
2484
+ overridden: info.overridesPackaged,
2485
+ description: workflow.description,
2486
+ metadata: workflow.metadata,
2487
+ inputs: workflow.inputs ?? {},
2488
+ output: workflow.output,
2489
+ nodes: Object.entries(workflowNodes).map(([nodeId, node]) => ({
2490
+ id: nodeId,
2491
+ kind: getNodeKind(node),
2492
+ needs: getNodeNeeds(node),
2493
+ agent: node.agent,
2494
+ agentsFrom: node.agentsFrom,
2495
+ action: node.action,
2496
+ control: node.control,
2497
+ if: node.if,
2498
+ input: node.input,
2499
+ with: node.with,
2500
+ output: node.output,
2501
+ writes: node.writes,
2502
+ json: node.json,
2503
+ routes: getWorkflowNodeRoutes(node),
2504
+ })),
2505
+ graph,
2506
+ };
2507
+ }
2508
+ export function showWorkflowGraph(config, workflowName, options = {}) {
2509
+ const workflow = config.workflows?.[workflowName];
2510
+ if (!workflow) {
2511
+ throw new Error(`Unknown workflow "${workflowName}".`);
2512
+ }
2513
+ const graph = buildWorkflowGraph(workflowName, workflow);
2514
+ const format = options.format ?? 'text';
2515
+ if (format === 'json') {
2516
+ console.log(JSON.stringify(graph, null, 2));
2517
+ }
2518
+ else if (format === 'mermaid') {
2519
+ console.log(formatWorkflowGraphMermaid(graph));
2520
+ }
2521
+ else {
2522
+ console.log(chalk.bold(`\nWorkflow Graph: ${graph.workflow}\n`));
2523
+ for (const node of graph.nodes) {
2524
+ const outgoing = graph.edges.filter((edge) => edge.source === node.id);
2525
+ const suffix = node.condition ? chalk.yellow(` if ${node.condition}`) : '';
2526
+ console.log(` ${chalk.white(node.id)} ${chalk.gray(`[${node.kind}]`)}${suffix}`);
2527
+ for (const edge of outgoing) {
2528
+ const edgeLabel = edge.label ? ` ${chalk.gray(`(${edge.label})`)}` : '';
2529
+ const marker = edge.kind === 'control' ? chalk.cyan('~>') : chalk.gray('->');
2530
+ console.log(` ${marker} ${edge.target}${edgeLabel}`);
2531
+ }
2532
+ }
2533
+ console.log('');
2534
+ }
2535
+ return graph;
2536
+ }
2537
+ export function showWorkflow(config, workflowName, options = {}) {
2538
+ const workflow = config.workflows?.[workflowName];
2539
+ if (!workflow) {
2540
+ throw new Error(`Unknown workflow "${workflowName}".`);
2541
+ }
2542
+ const detail = buildWorkflowDetail(workflowName, workflow, options.workingDir ?? process.cwd());
2543
+ if (options.json) {
2544
+ console.log(JSON.stringify(detail, null, 2));
2545
+ return detail;
2546
+ }
2547
+ const sourceLabel = detail.source === 'packaged' ? chalk.gray('packaged') : chalk.cyan('project');
2548
+ const overridden = detail.overridden ? ` ${chalk.yellow('(overrides packaged)')}` : '';
2549
+ console.log(chalk.bold(`\nWorkflow: ${detail.name}\n`));
2550
+ console.log(` Source: ${sourceLabel}${overridden}`);
2551
+ if (detail.description) {
2552
+ console.log(` Description: ${detail.description}`);
2553
+ }
2554
+ if (detail.output) {
2555
+ console.log(` Output: ${detail.output}`);
2556
+ }
2557
+ if (detail.metadata) {
2558
+ console.log(` Metadata: ${JSON.stringify(detail.metadata)}`);
2559
+ }
2560
+ console.log(chalk.bold('\nInputs:'));
2561
+ const inputEntries = Object.entries(detail.inputs);
2562
+ if (inputEntries.length === 0) {
2563
+ console.log(chalk.gray(' (none)'));
2564
+ }
2565
+ else {
2566
+ for (const [key, input] of inputEntries) {
2567
+ console.log(` ${key}: ${formatWorkflowInput(input)}`);
2568
+ }
2569
+ }
2570
+ console.log(chalk.bold('\nNodes:'));
2571
+ for (const node of detail.nodes) {
2572
+ console.log(` ${node.id} (${node.kind})`);
2573
+ if (node.needs.length > 0) {
2574
+ console.log(` needs: ${node.needs.join(', ')}`);
2575
+ }
2576
+ if (node.agent)
2577
+ console.log(` agent: ${node.agent}`);
2578
+ if (node.agentsFrom)
2579
+ console.log(` agentsFrom: ${node.agentsFrom}`);
2580
+ if (node.action)
2581
+ console.log(` action: ${node.action}`);
2582
+ if (node.control)
2583
+ console.log(` control: ${node.control}`);
2584
+ if (node.if)
2585
+ console.log(` if: ${node.if}`);
2586
+ if (node.output)
2587
+ console.log(` output: ${node.output}`);
2588
+ if (node.writes)
2589
+ console.log(` writes: ${node.writes}`);
2590
+ if (node.input)
2591
+ console.log(` input: ${node.input.split('\n')[0]}`);
2592
+ if (node.with && Object.keys(node.with).length > 0) {
2593
+ console.log(` with: ${JSON.stringify(node.with)}`);
2594
+ }
2595
+ if (node.routes && Object.keys(node.routes).length > 0) {
2596
+ console.log(` routes: ${JSON.stringify(node.routes)}`);
2597
+ }
2598
+ }
2599
+ console.log('');
2600
+ return detail;
2601
+ }
2602
+ function validateSingleWorkflow(config, workflowName, workingDir) {
2603
+ const workflow = config.workflows?.[workflowName];
2604
+ if (!workflow) {
2605
+ return { name: workflowName, valid: false, error: `Unknown workflow "${workflowName}".` };
2606
+ }
2607
+ try {
2608
+ const sourceInfo = loadWorkflowSourceInfo(workingDir);
2609
+ const workflowNodes = getWorkflowNodes(workflowName, workflow);
2610
+ const executionOrder = getWorkflowExecutionOrder(workflowNodes);
2611
+ return {
2612
+ name: workflowName,
2613
+ valid: true,
2614
+ source: sourceInfo[workflowName]?.source ?? 'packaged',
2615
+ waves: getWorkflowExecutionWaves(workflowNodes, executionOrder),
2616
+ };
2617
+ }
2618
+ catch (error) {
2619
+ return {
2620
+ name: workflowName,
2621
+ valid: false,
2622
+ error: error instanceof Error ? error.message : String(error),
2623
+ };
2624
+ }
2625
+ }
2626
+ export function validateWorkflows(config, workflowName, options = {}) {
2627
+ const workingDir = options.workingDir ?? process.cwd();
2628
+ const names = workflowName ? [workflowName] : Object.keys(config.workflows ?? {}).sort();
2629
+ const results = names.map((name) => validateSingleWorkflow(config, name, workingDir));
2630
+ if (options.json) {
2631
+ console.log(JSON.stringify(results, null, 2));
2632
+ return results;
2633
+ }
2634
+ console.log(chalk.bold('\nWorkflow Validation:\n'));
2635
+ for (const result of results) {
2636
+ if (result.valid) {
2637
+ console.log(` ${chalk.green('✓')} ${result.name}`);
2638
+ if (result.waves) {
2639
+ console.log(` waves: ${result.waves.map((wave) => `[${wave.join(', ')}]`).join(' -> ')}`);
2640
+ }
2641
+ }
2642
+ else {
2643
+ console.log(` ${chalk.red('✗')} ${result.name}`);
2644
+ console.log(` ${chalk.red(result.error ?? 'invalid workflow')}`);
2645
+ }
2646
+ }
2647
+ console.log('');
2648
+ return results;
2649
+ }
2650
+ /**
2651
+ * Default in-process {@link WorkflowExecutor}. Delegates to {@link runWorkflow}.
2652
+ *
2653
+ * The Temporal executor will implement the same interface so `drs workflow run`
2654
+ * can dispatch through either backend once `--executor temporal` is added.
2655
+ */
2656
+ export class LocalWorkflowExecutor {
2657
+ async run(config, workflowName, options = {}) {
2658
+ return runWorkflow(config, workflowName, options);
2659
+ }
2660
+ }
1229
2661
  //# sourceMappingURL=workflow.js.map