@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.
- package/.pi/agents/describe/pr-describer.md +14 -0
- package/.pi/agents/review/unified-reviewer.md +33 -7
- package/.pi/agents/task/agents-md-updater.md +3 -1
- package/.pi/agents/task/review-assistant.md +22 -0
- package/.pi/agents/task/review-issue-fixer.md +18 -1
- package/.pi/agents/visual/pr-explainer.md +205 -0
- package/.pi/workflows/github-pr-describe.yaml +10 -7
- package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +154 -0
- package/.pi/workflows/github-pr-post-comment.yaml +10 -10
- package/.pi/workflows/github-pr-review-post.yaml +18 -8
- package/.pi/workflows/github-pr-review.yaml +354 -7
- package/.pi/workflows/github-pr-show-changes.yaml +8 -8
- package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +103 -0
- package/.pi/workflows/github-pr-visual-explain.yaml +80 -0
- package/.pi/workflows/gitlab-mr-describe.yaml +8 -5
- package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +150 -0
- package/.pi/workflows/gitlab-mr-post-comment.yaml +8 -8
- package/.pi/workflows/gitlab-mr-review.yaml +354 -5
- package/.pi/workflows/gitlab-mr-show-changes.yaml +6 -6
- package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +100 -0
- package/.pi/workflows/gitlab-mr-visual-explain.yaml +78 -0
- package/.pi/workflows/local-fix-review-issues.yaml +89 -13
- package/.pi/workflows/local-review.yaml +15 -2
- package/.pi/workflows/local-update-agents-md.yaml +1 -1
- package/.pi/workflows/local-visual-explain.yaml +76 -0
- package/.pi/workflows/release-changelog-finalize.yaml +47 -0
- package/.pi/workflows/tag-changelog-update.yaml +4 -4
- package/.pi/workflows/temporal-control-smoke.yaml +63 -0
- package/README.md +130 -27
- package/dist/ci/runner.d.ts.map +1 -1
- package/dist/ci/runner.js +3 -1
- package/dist/ci/runner.js.map +1 -1
- package/dist/cli/index.js +258 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +6 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -10
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run-agent.d.ts +4 -7
- package/dist/cli/run-agent.d.ts.map +1 -1
- package/dist/cli/run-agent.js +4 -0
- package/dist/cli/run-agent.js.map +1 -1
- package/dist/cli/workflow-executor-selection.d.ts +3 -0
- package/dist/cli/workflow-executor-selection.d.ts.map +1 -0
- package/dist/cli/workflow-executor-selection.js +15 -0
- package/dist/cli/workflow-executor-selection.js.map +1 -0
- package/dist/cli/workflow.d.ts +76 -26
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +1624 -192
- package/dist/cli/workflow.js.map +1 -1
- package/dist/github/client.d.ts +12 -0
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +27 -0
- package/dist/github/client.js.map +1 -1
- package/dist/github/platform-adapter.d.ts +6 -1
- package/dist/github/platform-adapter.d.ts.map +1 -1
- package/dist/github/platform-adapter.js +84 -8
- package/dist/github/platform-adapter.js.map +1 -1
- package/dist/gitlab/client.d.ts +11 -0
- package/dist/gitlab/client.d.ts.map +1 -1
- package/dist/gitlab/client.js +11 -0
- package/dist/gitlab/client.js.map +1 -1
- package/dist/gitlab/platform-adapter.d.ts +3 -1
- package/dist/gitlab/platform-adapter.d.ts.map +1 -1
- package/dist/gitlab/platform-adapter.js +32 -1
- package/dist/gitlab/platform-adapter.js.map +1 -1
- package/dist/lib/agent-result.d.ts +8 -0
- package/dist/lib/agent-result.d.ts.map +1 -0
- package/dist/lib/agent-result.js +2 -0
- package/dist/lib/agent-result.js.map +1 -0
- package/dist/lib/comment-formatter.d.ts +8 -0
- package/dist/lib/comment-formatter.d.ts.map +1 -1
- package/dist/lib/comment-formatter.js +12 -4
- package/dist/lib/comment-formatter.js.map +1 -1
- package/dist/lib/comment-poster.d.ts.map +1 -1
- package/dist/lib/comment-poster.js +28 -1
- package/dist/lib/comment-poster.js.map +1 -1
- package/dist/lib/config.d.ts +71 -11
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +163 -28
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-compression.d.ts +10 -0
- package/dist/lib/context-compression.d.ts.map +1 -1
- package/dist/lib/context-compression.js +101 -13
- package/dist/lib/context-compression.js.map +1 -1
- package/dist/lib/context-loader.d.ts +2 -1
- package/dist/lib/context-loader.d.ts.map +1 -1
- package/dist/lib/context-loader.js +70 -1
- package/dist/lib/context-loader.js.map +1 -1
- package/dist/lib/describe-core.d.ts.map +1 -1
- package/dist/lib/describe-core.js +3 -2
- package/dist/lib/describe-core.js.map +1 -1
- package/dist/lib/diff-lines.d.ts +9 -0
- package/dist/lib/diff-lines.d.ts.map +1 -1
- package/dist/lib/diff-lines.js +17 -9
- package/dist/lib/diff-lines.js.map +1 -1
- package/dist/lib/exit.js +4 -4
- package/dist/lib/exit.js.map +1 -1
- package/dist/lib/html-artifact.d.ts +14 -0
- package/dist/lib/html-artifact.d.ts.map +1 -0
- package/dist/lib/html-artifact.js +59 -0
- package/dist/lib/html-artifact.js.map +1 -0
- package/dist/lib/issue-parser.js +3 -3
- package/dist/lib/issue-parser.js.map +1 -1
- package/dist/lib/json-output-schema.d.ts +70 -0
- package/dist/lib/json-output-schema.d.ts.map +1 -1
- package/dist/lib/json-output-schema.js +40 -0
- package/dist/lib/json-output-schema.js.map +1 -1
- package/dist/lib/output-paths.d.ts +0 -1
- package/dist/lib/output-paths.d.ts.map +1 -1
- package/dist/lib/output-paths.js +0 -1
- package/dist/lib/output-paths.js.map +1 -1
- package/dist/lib/platform-client.d.ts +26 -0
- package/dist/lib/platform-client.d.ts.map +1 -1
- package/dist/lib/project-setup.d.ts +10 -0
- package/dist/lib/project-setup.d.ts.map +1 -0
- package/dist/lib/project-setup.js +25 -0
- package/dist/lib/project-setup.js.map +1 -0
- package/dist/lib/review-artifact-store.d.ts +16 -0
- package/dist/lib/review-artifact-store.d.ts.map +1 -0
- package/dist/lib/review-artifact-store.js +84 -0
- package/dist/lib/review-artifact-store.js.map +1 -0
- package/dist/lib/review-artifact.d.ts +69 -0
- package/dist/lib/review-artifact.d.ts.map +1 -0
- package/dist/lib/review-artifact.js +172 -0
- package/dist/lib/review-artifact.js.map +1 -0
- package/dist/lib/review-core.d.ts +7 -4
- package/dist/lib/review-core.d.ts.map +1 -1
- package/dist/lib/review-core.js +82 -165
- package/dist/lib/review-core.js.map +1 -1
- package/dist/lib/review-orchestrator.d.ts +23 -0
- package/dist/lib/review-orchestrator.d.ts.map +1 -1
- package/dist/lib/review-orchestrator.js +20 -13
- package/dist/lib/review-orchestrator.js.map +1 -1
- package/dist/lib/review-parser.d.ts.map +1 -1
- package/dist/lib/review-parser.js +15 -12
- package/dist/lib/review-parser.js.map +1 -1
- package/dist/lib/review-usage.d.ts +4 -0
- package/dist/lib/review-usage.d.ts.map +1 -1
- package/dist/lib/review-usage.js +25 -0
- package/dist/lib/review-usage.js.map +1 -1
- package/dist/lib/skills.d.ts +16 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +140 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/trace-collector.d.ts +105 -0
- package/dist/lib/trace-collector.d.ts.map +1 -0
- package/dist/lib/trace-collector.js +255 -0
- package/dist/lib/trace-collector.js.map +1 -0
- package/dist/lib/trace-html.d.ts +3 -0
- package/dist/lib/trace-html.d.ts.map +1 -0
- package/dist/lib/trace-html.js +349 -0
- package/dist/lib/trace-html.js.map +1 -0
- package/dist/lib/workflow/artifact-store.d.ts +71 -0
- package/dist/lib/workflow/artifact-store.d.ts.map +1 -0
- package/dist/lib/workflow/artifact-store.js +121 -0
- package/dist/lib/workflow/artifact-store.js.map +1 -0
- package/dist/lib/workflow/compiled-plan.d.ts +87 -0
- package/dist/lib/workflow/compiled-plan.d.ts.map +1 -0
- package/dist/lib/workflow/compiled-plan.js +62 -0
- package/dist/lib/workflow/compiled-plan.js.map +1 -0
- package/dist/lib/workflow/executor.d.ts +18 -0
- package/dist/lib/workflow/executor.d.ts.map +1 -0
- package/dist/lib/workflow/executor.js +2 -0
- package/dist/lib/workflow/executor.js.map +1 -0
- package/dist/lib/workflow/graph.d.ts +30 -0
- package/dist/lib/workflow/graph.d.ts.map +1 -0
- package/dist/lib/workflow/graph.js +139 -0
- package/dist/lib/workflow/graph.js.map +1 -0
- package/dist/lib/workflow/input.d.ts +4 -0
- package/dist/lib/workflow/input.d.ts.map +1 -0
- package/dist/lib/workflow/input.js +4 -0
- package/dist/lib/workflow/input.js.map +1 -0
- package/dist/lib/workflow/node-executor.d.ts +19 -0
- package/dist/lib/workflow/node-executor.d.ts.map +1 -0
- package/dist/lib/workflow/node-executor.js +2 -0
- package/dist/lib/workflow/node-executor.js.map +1 -0
- package/dist/lib/workflow/planning.d.ts +51 -0
- package/dist/lib/workflow/planning.d.ts.map +1 -0
- package/dist/lib/workflow/planning.js +814 -0
- package/dist/lib/workflow/planning.js.map +1 -0
- package/dist/lib/workflow/types.d.ts +66 -0
- package/dist/lib/workflow/types.d.ts.map +1 -0
- package/dist/lib/workflow/types.js +2 -0
- package/dist/lib/workflow/types.js.map +1 -0
- package/dist/lib/workflow-artifacts.d.ts +54 -0
- package/dist/lib/workflow-artifacts.d.ts.map +1 -0
- package/dist/lib/workflow-artifacts.js +150 -0
- package/dist/lib/workflow-artifacts.js.map +1 -0
- package/dist/lib/write-json-output.d.ts.map +1 -1
- package/dist/lib/write-json-output.js +3 -8
- package/dist/lib/write-json-output.js.map +1 -1
- package/dist/pi/sdk.d.ts.map +1 -1
- package/dist/pi/sdk.js +576 -8
- package/dist/pi/sdk.js.map +1 -1
- package/dist/runtime/agent-loader.js +2 -2
- package/dist/runtime/built-in-paths.d.ts +1 -0
- package/dist/runtime/built-in-paths.d.ts.map +1 -1
- package/dist/runtime/built-in-paths.js +7 -0
- package/dist/runtime/built-in-paths.js.map +1 -1
- package/dist/runtime/client.d.ts +2 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +11 -5
- package/dist/runtime/client.js.map +1 -1
- package/dist/temporal/activities.d.ts +26 -0
- package/dist/temporal/activities.d.ts.map +1 -0
- package/dist/temporal/activities.js +219 -0
- package/dist/temporal/activities.js.map +1 -0
- package/dist/temporal/config.d.ts +4 -0
- package/dist/temporal/config.d.ts.map +1 -0
- package/dist/temporal/config.js +36 -0
- package/dist/temporal/config.js.map +1 -0
- package/dist/temporal/executor.d.ts +7 -0
- package/dist/temporal/executor.d.ts.map +1 -0
- package/dist/temporal/executor.js +233 -0
- package/dist/temporal/executor.js.map +1 -0
- package/dist/temporal/retry-policy.d.ts +28 -0
- package/dist/temporal/retry-policy.d.ts.map +1 -0
- package/dist/temporal/retry-policy.js +56 -0
- package/dist/temporal/retry-policy.js.map +1 -0
- package/dist/temporal/types.d.ts +86 -0
- package/dist/temporal/types.d.ts.map +1 -0
- package/dist/temporal/types.js +2 -0
- package/dist/temporal/types.js.map +1 -0
- package/dist/temporal/worker.d.ts +3 -0
- package/dist/temporal/worker.d.ts.map +1 -0
- package/dist/temporal/worker.js +18 -0
- package/dist/temporal/worker.js.map +1 -0
- package/dist/temporal/workflow-id.d.ts +27 -0
- package/dist/temporal/workflow-id.d.ts.map +1 -0
- package/dist/temporal/workflow-id.js +53 -0
- package/dist/temporal/workflow-id.js.map +1 -0
- package/dist/temporal/workflows.d.ts +6 -0
- package/dist/temporal/workflows.d.ts.map +1 -0
- package/dist/temporal/workflows.js +342 -0
- package/dist/temporal/workflows.js.map +1 -0
- package/package.json +26 -15
- package/.pi/workflows/github-pr-describe-post.yaml +0 -24
- package/.pi/workflows/gitlab-mr-describe-post.yaml +0 -22
- package/.pi/workflows/gitlab-mr-review-code-quality.yaml +0 -31
- package/.pi/workflows/gitlab-mr-review-post-code-quality.yaml +0 -40
- package/.pi/workflows/gitlab-mr-review-post.yaml +0 -30
- package/.pi/workflows/local-staged-review.yaml +0 -17
- package/dist/cli/run-agent.test.d.ts +0 -2
- package/dist/cli/run-agent.test.d.ts.map +0 -1
- package/dist/cli/run-agent.test.js +0 -204
- package/dist/cli/run-agent.test.js.map +0 -1
- package/dist/cli/workflow.test.d.ts +0 -2
- package/dist/cli/workflow.test.d.ts.map +0 -1
- package/dist/cli/workflow.test.js +0 -1410
- package/dist/cli/workflow.test.js.map +0 -1
- package/dist/github/client.test.d.ts +0 -2
- package/dist/github/client.test.d.ts.map +0 -1
- package/dist/github/client.test.js +0 -206
- package/dist/github/client.test.js.map +0 -1
- package/dist/github/platform-adapter.test.d.ts +0 -2
- package/dist/github/platform-adapter.test.d.ts.map +0 -1
- package/dist/github/platform-adapter.test.js +0 -40
- package/dist/github/platform-adapter.test.js.map +0 -1
- package/dist/gitlab/diff-parser.test.d.ts +0 -2
- package/dist/gitlab/diff-parser.test.d.ts.map +0 -1
- package/dist/gitlab/diff-parser.test.js +0 -315
- package/dist/gitlab/diff-parser.test.js.map +0 -1
- package/dist/gitlab/platform-adapter.test.d.ts +0 -2
- package/dist/gitlab/platform-adapter.test.d.ts.map +0 -1
- package/dist/gitlab/platform-adapter.test.js +0 -21
- package/dist/gitlab/platform-adapter.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -7
- package/dist/index.test.js.map +0 -1
- package/dist/lib/code-quality-report.test.d.ts +0 -2
- package/dist/lib/code-quality-report.test.d.ts.map +0 -1
- package/dist/lib/code-quality-report.test.js +0 -327
- package/dist/lib/code-quality-report.test.js.map +0 -1
- package/dist/lib/comment-formatter.test.d.ts +0 -2
- package/dist/lib/comment-formatter.test.d.ts.map +0 -1
- package/dist/lib/comment-formatter.test.js +0 -727
- package/dist/lib/comment-formatter.test.js.map +0 -1
- package/dist/lib/comment-manager.test.d.ts +0 -2
- package/dist/lib/comment-manager.test.d.ts.map +0 -1
- package/dist/lib/comment-manager.test.js +0 -680
- package/dist/lib/comment-manager.test.js.map +0 -1
- package/dist/lib/comment-poster.test.d.ts +0 -5
- package/dist/lib/comment-poster.test.d.ts.map +0 -1
- package/dist/lib/comment-poster.test.js +0 -255
- package/dist/lib/comment-poster.test.js.map +0 -1
- package/dist/lib/config-model-overrides.test.d.ts +0 -2
- package/dist/lib/config-model-overrides.test.d.ts.map +0 -1
- package/dist/lib/config-model-overrides.test.js +0 -218
- package/dist/lib/config-model-overrides.test.js.map +0 -1
- package/dist/lib/config.test.d.ts +0 -2
- package/dist/lib/config.test.d.ts.map +0 -1
- package/dist/lib/config.test.js +0 -353
- package/dist/lib/config.test.js.map +0 -1
- package/dist/lib/context-compression.test.d.ts +0 -2
- package/dist/lib/context-compression.test.d.ts.map +0 -1
- package/dist/lib/context-compression.test.js +0 -337
- package/dist/lib/context-compression.test.js.map +0 -1
- package/dist/lib/context-loader.test.d.ts +0 -2
- package/dist/lib/context-loader.test.d.ts.map +0 -1
- package/dist/lib/context-loader.test.js +0 -212
- package/dist/lib/context-loader.test.js.map +0 -1
- package/dist/lib/cursor-fix-link.test.d.ts +0 -2
- package/dist/lib/cursor-fix-link.test.d.ts.map +0 -1
- package/dist/lib/cursor-fix-link.test.js +0 -70
- package/dist/lib/cursor-fix-link.test.js.map +0 -1
- package/dist/lib/describe-core.test.d.ts +0 -2
- package/dist/lib/describe-core.test.d.ts.map +0 -1
- package/dist/lib/describe-core.test.js +0 -208
- package/dist/lib/describe-core.test.js.map +0 -1
- package/dist/lib/describe-output-path.test.d.ts +0 -2
- package/dist/lib/describe-output-path.test.d.ts.map +0 -1
- package/dist/lib/describe-output-path.test.js +0 -51
- package/dist/lib/describe-output-path.test.js.map +0 -1
- package/dist/lib/describe-parser.test.d.ts +0 -2
- package/dist/lib/describe-parser.test.d.ts.map +0 -1
- package/dist/lib/describe-parser.test.js +0 -282
- package/dist/lib/describe-parser.test.js.map +0 -1
- package/dist/lib/description-executor.test.d.ts +0 -2
- package/dist/lib/description-executor.test.d.ts.map +0 -1
- package/dist/lib/description-executor.test.js +0 -135
- package/dist/lib/description-executor.test.js.map +0 -1
- package/dist/lib/description-formatter.test.d.ts +0 -2
- package/dist/lib/description-formatter.test.d.ts.map +0 -1
- package/dist/lib/description-formatter.test.js +0 -57
- package/dist/lib/description-formatter.test.js.map +0 -1
- package/dist/lib/diff-lines.test.d.ts +0 -2
- package/dist/lib/diff-lines.test.d.ts.map +0 -1
- package/dist/lib/diff-lines.test.js +0 -13
- package/dist/lib/diff-lines.test.js.map +0 -1
- package/dist/lib/diff-parser.test.d.ts +0 -2
- package/dist/lib/diff-parser.test.d.ts.map +0 -1
- package/dist/lib/diff-parser.test.js +0 -335
- package/dist/lib/diff-parser.test.js.map +0 -1
- package/dist/lib/error-comment-poster.test.d.ts +0 -2
- package/dist/lib/error-comment-poster.test.d.ts.map +0 -1
- package/dist/lib/error-comment-poster.test.js +0 -128
- package/dist/lib/error-comment-poster.test.js.map +0 -1
- package/dist/lib/exit.test.d.ts +0 -2
- package/dist/lib/exit.test.d.ts.map +0 -1
- package/dist/lib/exit.test.js +0 -120
- package/dist/lib/exit.test.js.map +0 -1
- package/dist/lib/issue-parser.test.d.ts +0 -2
- package/dist/lib/issue-parser.test.d.ts.map +0 -1
- package/dist/lib/issue-parser.test.js +0 -281
- package/dist/lib/issue-parser.test.js.map +0 -1
- package/dist/lib/json-output-schema.test.d.ts +0 -2
- package/dist/lib/json-output-schema.test.d.ts.map +0 -1
- package/dist/lib/json-output-schema.test.js +0 -92
- package/dist/lib/json-output-schema.test.js.map +0 -1
- package/dist/lib/json-output.test.d.ts +0 -2
- package/dist/lib/json-output.test.d.ts.map +0 -1
- package/dist/lib/json-output.test.js +0 -141
- package/dist/lib/json-output.test.js.map +0 -1
- package/dist/lib/logger.test.d.ts +0 -2
- package/dist/lib/logger.test.d.ts.map +0 -1
- package/dist/lib/logger.test.js +0 -324
- package/dist/lib/logger.test.js.map +0 -1
- package/dist/lib/position-validator.test.d.ts +0 -2
- package/dist/lib/position-validator.test.d.ts.map +0 -1
- package/dist/lib/position-validator.test.js +0 -128
- package/dist/lib/position-validator.test.js.map +0 -1
- package/dist/lib/prompt-budget.test.d.ts +0 -2
- package/dist/lib/prompt-budget.test.d.ts.map +0 -1
- package/dist/lib/prompt-budget.test.js +0 -55
- package/dist/lib/prompt-budget.test.js.map +0 -1
- package/dist/lib/repository-validator.test.d.ts +0 -5
- package/dist/lib/repository-validator.test.d.ts.map +0 -1
- package/dist/lib/repository-validator.test.js +0 -341
- package/dist/lib/repository-validator.test.js.map +0 -1
- package/dist/lib/review-core.test.d.ts +0 -2
- package/dist/lib/review-core.test.d.ts.map +0 -1
- package/dist/lib/review-core.test.js +0 -614
- package/dist/lib/review-core.test.js.map +0 -1
- package/dist/lib/review-orchestrator.test.d.ts +0 -2
- package/dist/lib/review-orchestrator.test.d.ts.map +0 -1
- package/dist/lib/review-orchestrator.test.js +0 -552
- package/dist/lib/review-orchestrator.test.js.map +0 -1
- package/dist/lib/review-output-path.test.d.ts +0 -2
- package/dist/lib/review-output-path.test.d.ts.map +0 -1
- package/dist/lib/review-output-path.test.js +0 -83
- package/dist/lib/review-output-path.test.js.map +0 -1
- package/dist/lib/review-parser.test.d.ts +0 -2
- package/dist/lib/review-parser.test.d.ts.map +0 -1
- package/dist/lib/review-parser.test.js +0 -130
- package/dist/lib/review-parser.test.js.map +0 -1
- package/dist/lib/review-usage.test.d.ts +0 -2
- package/dist/lib/review-usage.test.d.ts.map +0 -1
- package/dist/lib/review-usage.test.js +0 -83
- package/dist/lib/review-usage.test.js.map +0 -1
- package/dist/lib/unified-review-executor.d.ts +0 -58
- package/dist/lib/unified-review-executor.d.ts.map +0 -1
- package/dist/lib/unified-review-executor.js +0 -201
- package/dist/lib/unified-review-executor.js.map +0 -1
- package/dist/lib/unified-review-executor.test.d.ts +0 -5
- package/dist/lib/unified-review-executor.test.d.ts.map +0 -1
- package/dist/lib/unified-review-executor.test.js +0 -472
- package/dist/lib/unified-review-executor.test.js.map +0 -1
- package/dist/lib/write-json-output.test.d.ts +0 -2
- package/dist/lib/write-json-output.test.d.ts.map +0 -1
- package/dist/lib/write-json-output.test.js +0 -259
- package/dist/lib/write-json-output.test.js.map +0 -1
- package/dist/pi/sdk.test.d.ts +0 -2
- package/dist/pi/sdk.test.d.ts.map +0 -1
- package/dist/pi/sdk.test.js +0 -488
- package/dist/pi/sdk.test.js.map +0 -1
- package/dist/runtime/agent-loader.test.d.ts +0 -2
- package/dist/runtime/agent-loader.test.d.ts.map +0 -1
- package/dist/runtime/agent-loader.test.js +0 -277
- package/dist/runtime/agent-loader.test.js.map +0 -1
- package/dist/runtime/client.test.d.ts +0 -2
- package/dist/runtime/client.test.d.ts.map +0 -1
- package/dist/runtime/client.test.js +0 -772
- package/dist/runtime/client.test.js.map +0 -1
- package/dist/runtime/path-config.test.d.ts +0 -2
- package/dist/runtime/path-config.test.d.ts.map +0 -1
- package/dist/runtime/path-config.test.js +0 -112
- package/dist/runtime/path-config.test.js.map +0 -1
package/dist/cli/workflow.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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') ||
|
|
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
|
|
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
|
-
...
|
|
1066
|
-
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
|
|
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
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
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
|