agentweaver 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +26 -9
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +15 -0
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/flow-readiness.js +15 -18
  6. package/dist/flow-state.js +75 -15
  7. package/dist/index.js +391 -175
  8. package/dist/interactive/blessed-session.js +361 -0
  9. package/dist/interactive/controller.js +1293 -0
  10. package/dist/interactive/create-interactive-session.js +5 -0
  11. package/dist/interactive/ink/index.js +576 -0
  12. package/dist/interactive/progress.js +245 -0
  13. package/dist/interactive/selectors.js +14 -0
  14. package/dist/interactive/session.js +1 -0
  15. package/dist/interactive/state.js +34 -0
  16. package/dist/interactive/tree.js +155 -0
  17. package/dist/interactive/types.js +1 -0
  18. package/dist/interactive/view-model.js +1 -0
  19. package/dist/interactive-ui.js +159 -194
  20. package/dist/pipeline/context.js +1 -0
  21. package/dist/pipeline/declarative-flow-runner.js +212 -6
  22. package/dist/pipeline/declarative-flows.js +27 -0
  23. package/dist/pipeline/execution-routing-config.js +15 -0
  24. package/dist/pipeline/flow-catalog.js +19 -3
  25. package/dist/pipeline/flow-run-resume.js +29 -0
  26. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  27. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  28. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  29. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  30. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  31. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  32. package/dist/pipeline/flow-specs/design-review.json +10 -0
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  35. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  36. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  37. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  38. package/dist/pipeline/flow-specs/implement.json +13 -6
  39. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  40. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  41. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  42. package/dist/pipeline/flow-specs/plan.json +48 -70
  43. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  44. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  45. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  46. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  47. package/dist/pipeline/flow-specs/review/review.json +37 -31
  48. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  49. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  50. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  51. package/dist/pipeline/node-registry.js +41 -1
  52. package/dist/pipeline/node-runner.js +3 -2
  53. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  54. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  55. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  56. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  57. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  58. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  59. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  60. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  61. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  62. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  63. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  64. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  65. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  66. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  67. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  68. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  69. package/dist/pipeline/nodes/user-input-node.js +38 -3
  70. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  71. package/dist/pipeline/prompt-registry.js +3 -1
  72. package/dist/pipeline/prompt-runtime.js +4 -1
  73. package/dist/pipeline/review-iteration.js +26 -0
  74. package/dist/pipeline/spec-compiler.js +2 -0
  75. package/dist/pipeline/spec-types.js +3 -0
  76. package/dist/pipeline/spec-validator.js +14 -0
  77. package/dist/pipeline/value-resolver.js +74 -1
  78. package/dist/prompts.js +36 -14
  79. package/dist/review-severity.js +45 -0
  80. package/dist/runtime/artifact-registry.js +402 -0
  81. package/dist/runtime/design-review-input-contract.js +17 -16
  82. package/dist/runtime/env-loader.js +3 -0
  83. package/dist/runtime/execution-routing-store.js +134 -0
  84. package/dist/runtime/execution-routing.js +227 -0
  85. package/dist/runtime/interactive-execution-routing.js +462 -0
  86. package/dist/runtime/plan-revise-input-contract.js +35 -32
  87. package/dist/runtime/planning-bundle.js +123 -0
  88. package/dist/runtime/ready-to-merge.js +22 -1
  89. package/dist/runtime/review-input-contract.js +100 -0
  90. package/dist/structured-artifact-schema-registry.js +9 -0
  91. package/dist/structured-artifact-schemas.json +140 -1
  92. package/dist/structured-artifacts.js +77 -6
  93. package/dist/user-input.js +70 -3
  94. package/package.json +6 -3
@@ -1,3 +1,4 @@
1
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
1
2
  import { toExecutorContext } from "../types.js";
2
3
  export const fetchGitLabDiffNode = {
3
4
  kind: "fetch-gitlab-diff",
@@ -12,8 +13,24 @@ export const fetchGitLabDiffNode = {
12
13
  return {
13
14
  value,
14
15
  outputs: [
15
- { kind: "artifact", path: params.outputFile, required: true },
16
- { kind: "artifact", path: params.outputJsonFile, required: true },
16
+ {
17
+ kind: "artifact",
18
+ path: params.outputFile,
19
+ required: true,
20
+ manifest: {
21
+ publish: true,
22
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
23
+ },
24
+ },
25
+ {
26
+ kind: "artifact",
27
+ path: params.outputJsonFile,
28
+ required: true,
29
+ manifest: {
30
+ publish: true,
31
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputJsonFile),
32
+ },
33
+ },
17
34
  ],
18
35
  };
19
36
  },
@@ -1,3 +1,4 @@
1
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
1
2
  import { toExecutorContext } from "../types.js";
2
3
  export const fetchGitLabReviewNode = {
3
4
  kind: "fetch-gitlab-review",
@@ -12,8 +13,24 @@ export const fetchGitLabReviewNode = {
12
13
  return {
13
14
  value,
14
15
  outputs: [
15
- { kind: "artifact", path: params.outputFile, required: true },
16
- { kind: "artifact", path: params.outputJsonFile, required: true },
16
+ {
17
+ kind: "artifact",
18
+ path: params.outputFile,
19
+ required: true,
20
+ manifest: {
21
+ publish: true,
22
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
23
+ },
24
+ },
25
+ {
26
+ kind: "artifact",
27
+ path: params.outputJsonFile,
28
+ required: true,
29
+ manifest: {
30
+ publish: true,
31
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputJsonFile),
32
+ },
33
+ },
17
34
  ],
18
35
  };
19
36
  },
@@ -1,6 +1,25 @@
1
1
  import { printInfo } from "../../tui.js";
2
+ import { resolveDesignReviewInputContract } from "../../runtime/design-review-input-contract.js";
3
+ import { resolvePlanReviseInputContract } from "../../runtime/plan-revise-input-contract.js";
4
+ import { inspectReviewInputContract } from "../../runtime/review-input-contract.js";
2
5
  import { runExpandedPhase } from "../declarative-flow-runner.js";
3
6
  import { loadNamedDeclarativeFlow } from "../declarative-flows.js";
7
+ import { isFlowRunResumeEnvelope } from "../flow-run-resume.js";
8
+ import { withCanonicalReviewLoopParams } from "../review-iteration.js";
9
+ import { ARTIFACT_LINEAGE_REF_PATHS_PARAM } from "../value-resolver.js";
10
+ function withArtifactLineageRefPaths(params, lineageRefs) {
11
+ if (Object.keys(lineageRefs).length === 0) {
12
+ return params;
13
+ }
14
+ const existing = params[ARTIFACT_LINEAGE_REF_PATHS_PARAM];
15
+ const merged = existing && typeof existing === "object" && !Array.isArray(existing)
16
+ ? { ...existing, ...lineageRefs }
17
+ : lineageRefs;
18
+ return {
19
+ ...params,
20
+ [ARTIFACT_LINEAGE_REF_PATHS_PARAM]: merged,
21
+ };
22
+ }
4
23
  export const flowRunNode = {
5
24
  kind: "flow-run",
6
25
  version: 1,
@@ -13,28 +32,228 @@ export const flowRunNode = {
13
32
  printInfo(String(labelText));
14
33
  }
15
34
  const flow = loadNamedDeclarativeFlow(fileName, context.cwd);
16
- const executionState = {
35
+ let resolvedFlowParams = withCanonicalReviewLoopParams(flow.kind, flowParams);
36
+ if (flow.kind === "design-review-flow") {
37
+ const taskKey = String(flowParams["taskKey"] ?? "");
38
+ if (taskKey) {
39
+ const contract = resolveDesignReviewInputContract(taskKey);
40
+ resolvedFlowParams = withArtifactLineageRefPaths({
41
+ ...flowParams,
42
+ iteration: contract.planningIteration,
43
+ planningIteration: contract.planningIteration,
44
+ designFile: contract.designFile,
45
+ designJsonFile: contract.designJsonFile,
46
+ planFile: contract.planFile,
47
+ planJsonFile: contract.planJsonFile,
48
+ hasQaArtifacts: contract.hasQaArtifacts,
49
+ qaFilePath: contract.qaFilePath,
50
+ qaJsonFilePath: contract.qaJsonFilePath,
51
+ qaFile: contract.qaFile,
52
+ qaJsonFile: contract.qaJsonFile,
53
+ hasTaskContextJsonFile: contract.hasTaskContextJsonFile,
54
+ taskContextJsonFilePath: contract.taskContextJsonFilePath,
55
+ taskContextJsonFile: contract.taskContextJsonFile,
56
+ hasJiraTaskFile: contract.hasJiraTaskFile,
57
+ jiraTaskFilePath: contract.jiraTaskFilePath,
58
+ jiraTaskFile: contract.jiraTaskFile,
59
+ hasJiraAttachmentsManifestFile: contract.hasJiraAttachmentsManifestFile,
60
+ jiraAttachmentsManifestFilePath: contract.jiraAttachmentsManifestFilePath,
61
+ jiraAttachmentsManifestFile: contract.jiraAttachmentsManifestFile,
62
+ hasJiraAttachmentsContextFile: contract.hasJiraAttachmentsContextFile,
63
+ jiraAttachmentsContextFilePath: contract.jiraAttachmentsContextFilePath,
64
+ jiraAttachmentsContextFile: contract.jiraAttachmentsContextFile,
65
+ hasPlanningAnswersJsonFile: contract.hasPlanningAnswersJsonFile,
66
+ planningAnswersJsonFilePath: contract.planningAnswersJsonFilePath,
67
+ planningAnswersJsonFile: contract.planningAnswersJsonFile,
68
+ hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
69
+ taskInputJsonFilePath: contract.taskInputJsonFilePath,
70
+ taskInputJsonFile: contract.taskInputJsonFile,
71
+ }, {
72
+ "params.designFile": contract.designFile,
73
+ "params.designJsonFile": contract.designJsonFile,
74
+ "params.planFile": contract.planFile,
75
+ "params.planJsonFile": contract.planJsonFile,
76
+ ...(contract.qaFilePath ? { "params.qaFile": contract.qaFilePath } : {}),
77
+ ...(contract.qaJsonFilePath ? { "params.qaJsonFile": contract.qaJsonFilePath } : {}),
78
+ ...(contract.taskContextJsonFilePath
79
+ ? { "params.taskContextJsonFile": contract.taskContextJsonFilePath }
80
+ : {}),
81
+ ...(contract.jiraTaskFilePath ? { "params.jiraTaskFile": contract.jiraTaskFilePath } : {}),
82
+ ...(contract.jiraAttachmentsManifestFilePath
83
+ ? { "params.jiraAttachmentsManifestFile": contract.jiraAttachmentsManifestFilePath }
84
+ : {}),
85
+ ...(contract.jiraAttachmentsContextFilePath
86
+ ? { "params.jiraAttachmentsContextFile": contract.jiraAttachmentsContextFilePath }
87
+ : {}),
88
+ ...(contract.planningAnswersJsonFilePath
89
+ ? { "params.planningAnswersJsonFile": contract.planningAnswersJsonFilePath }
90
+ : {}),
91
+ ...(contract.taskInputJsonFilePath
92
+ ? { "params.taskInputJsonFile": contract.taskInputJsonFilePath }
93
+ : {}),
94
+ });
95
+ }
96
+ }
97
+ else if (flow.kind === "plan-revise-flow") {
98
+ const taskKey = String(flowParams["taskKey"] ?? "");
99
+ if (taskKey) {
100
+ const contract = resolvePlanReviseInputContract(taskKey);
101
+ resolvedFlowParams = withArtifactLineageRefPaths({
102
+ ...flowParams,
103
+ reviewIteration: contract.reviewIteration,
104
+ reviewFile: contract.reviewFile,
105
+ reviewJsonFile: contract.reviewJsonFile,
106
+ sourcePlanningIteration: contract.sourcePlanningIteration,
107
+ outputIteration: contract.outputIteration,
108
+ designFile: contract.designFile,
109
+ designJsonFile: contract.designJsonFile,
110
+ planFile: contract.planFile,
111
+ planJsonFile: contract.planJsonFile,
112
+ hasQaArtifacts: contract.hasQaArtifacts,
113
+ qaFilePath: contract.qaFilePath,
114
+ qaJsonFilePath: contract.qaJsonFilePath,
115
+ qaFile: contract.qaFile,
116
+ qaJsonFile: contract.qaJsonFile,
117
+ revisedDesignFile: contract.revisedDesignFile,
118
+ revisedDesignJsonFile: contract.revisedDesignJsonFile,
119
+ revisedPlanFile: contract.revisedPlanFile,
120
+ revisedPlanJsonFile: contract.revisedPlanJsonFile,
121
+ revisedQaFile: contract.revisedQaFile,
122
+ revisedQaJsonFile: contract.revisedQaJsonFile,
123
+ hasTaskContextJsonFile: contract.hasTaskContextJsonFile,
124
+ taskContextJsonFilePath: contract.taskContextJsonFilePath,
125
+ taskContextJsonFile: contract.taskContextJsonFile,
126
+ hasJiraTaskFile: contract.hasJiraTaskFile,
127
+ jiraTaskFilePath: contract.jiraTaskFilePath,
128
+ jiraTaskFile: contract.jiraTaskFile,
129
+ hasJiraAttachmentsManifestFile: contract.hasJiraAttachmentsManifestFile,
130
+ jiraAttachmentsManifestFilePath: contract.jiraAttachmentsManifestFilePath,
131
+ jiraAttachmentsManifestFile: contract.jiraAttachmentsManifestFile,
132
+ hasJiraAttachmentsContextFile: contract.hasJiraAttachmentsContextFile,
133
+ jiraAttachmentsContextFilePath: contract.jiraAttachmentsContextFilePath,
134
+ jiraAttachmentsContextFile: contract.jiraAttachmentsContextFile,
135
+ hasPlanningAnswersJsonFile: contract.hasPlanningAnswersJsonFile,
136
+ planningAnswersJsonFilePath: contract.planningAnswersJsonFilePath,
137
+ planningAnswersJsonFile: contract.planningAnswersJsonFile,
138
+ hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
139
+ taskInputJsonFilePath: contract.taskInputJsonFilePath,
140
+ taskInputJsonFile: contract.taskInputJsonFile,
141
+ }, {
142
+ "params.reviewFile": contract.reviewFile,
143
+ "params.reviewJsonFile": contract.reviewJsonFile,
144
+ "params.designFile": contract.designFile,
145
+ "params.designJsonFile": contract.designJsonFile,
146
+ "params.planFile": contract.planFile,
147
+ "params.planJsonFile": contract.planJsonFile,
148
+ ...(contract.qaFilePath ? { "params.qaFile": contract.qaFilePath } : {}),
149
+ ...(contract.qaJsonFilePath ? { "params.qaJsonFile": contract.qaJsonFilePath } : {}),
150
+ ...(contract.taskContextJsonFilePath
151
+ ? { "params.taskContextJsonFile": contract.taskContextJsonFilePath }
152
+ : {}),
153
+ ...(contract.jiraTaskFilePath ? { "params.jiraTaskFile": contract.jiraTaskFilePath } : {}),
154
+ ...(contract.jiraAttachmentsManifestFilePath
155
+ ? { "params.jiraAttachmentsManifestFile": contract.jiraAttachmentsManifestFilePath }
156
+ : {}),
157
+ ...(contract.jiraAttachmentsContextFilePath
158
+ ? { "params.jiraAttachmentsContextFile": contract.jiraAttachmentsContextFilePath }
159
+ : {}),
160
+ ...(contract.planningAnswersJsonFilePath
161
+ ? { "params.planningAnswersJsonFile": contract.planningAnswersJsonFilePath }
162
+ : {}),
163
+ ...(contract.taskInputJsonFilePath
164
+ ? { "params.taskInputJsonFile": contract.taskInputJsonFilePath }
165
+ : {}),
166
+ });
167
+ }
168
+ }
169
+ else if (flow.kind === "review-flow") {
170
+ const taskKey = String(flowParams["taskKey"] ?? "");
171
+ if (taskKey) {
172
+ const inspection = inspectReviewInputContract(taskKey);
173
+ if (inspection.status === "ready") {
174
+ const { contract } = inspection;
175
+ resolvedFlowParams = withArtifactLineageRefPaths({
176
+ ...flowParams,
177
+ planningIteration: contract.planningIteration,
178
+ designFile: contract.designFile,
179
+ designJsonFile: contract.designJsonFile,
180
+ planFile: contract.planFile,
181
+ planJsonFile: contract.planJsonFile,
182
+ hasTaskContextJsonFile: contract.hasTaskContextJsonFile,
183
+ taskContextJsonFilePath: contract.taskContextJsonFilePath,
184
+ taskContextJsonFile: contract.taskContextJsonFile,
185
+ hasJiraTaskFile: contract.hasJiraTaskFile,
186
+ jiraTaskFilePath: contract.jiraTaskFilePath,
187
+ jiraTaskFile: contract.jiraTaskFile,
188
+ hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
189
+ taskInputJsonFilePath: contract.taskInputJsonFilePath,
190
+ taskInputJsonFile: contract.taskInputJsonFile,
191
+ }, {
192
+ "params.designFile": contract.designFile,
193
+ "params.designJsonFile": contract.designJsonFile,
194
+ "params.planFile": contract.planFile,
195
+ "params.planJsonFile": contract.planJsonFile,
196
+ ...(contract.taskContextJsonFilePath
197
+ ? { "params.taskContextJsonFile": contract.taskContextJsonFilePath }
198
+ : {}),
199
+ ...(contract.jiraTaskFilePath ? { "params.jiraTaskFile": contract.jiraTaskFilePath } : {}),
200
+ ...(contract.taskInputJsonFilePath
201
+ ? { "params.taskInputJsonFile": contract.taskInputJsonFilePath }
202
+ : {}),
203
+ });
204
+ }
205
+ }
206
+ }
207
+ const resumeValue = isFlowRunResumeEnvelope(context.resumeStepValue)
208
+ && context.resumeStepValue.flowKind === flow.kind
209
+ && context.resumeStepValue.flowVersion === flow.version
210
+ ? context.resumeStepValue
211
+ : null;
212
+ const executionState = resumeValue?.executionState ?? {
17
213
  flowKind: flow.kind,
18
214
  flowVersion: flow.version,
19
215
  terminated: false,
216
+ terminationOutcome: "success",
20
217
  phases: [],
21
218
  };
219
+ const buildResumeEnvelope = (nextExecutionState) => ({
220
+ resumeKind: "flow-run",
221
+ flowKind: flow.kind,
222
+ flowVersion: flow.version,
223
+ executionState: nextExecutionState,
224
+ publishedArtifacts: collectPublishedArtifacts(nextExecutionState),
225
+ });
22
226
  for (const phase of flow.phases) {
23
- await runExpandedPhase(phase, context, flowParams, flow.constants, {
227
+ await runExpandedPhase(phase, context, resolvedFlowParams, flow.constants, {
24
228
  executionState,
25
229
  flowKind: flow.kind,
26
230
  flowVersion: flow.version,
231
+ onStateChange: async (nextExecutionState) => {
232
+ await context.persistRunningStepValue?.(buildResumeEnvelope(nextExecutionState));
233
+ },
27
234
  });
28
235
  if (executionState.terminated) {
29
236
  break;
30
237
  }
31
238
  }
32
239
  return {
33
- value: {
34
- flowKind: flow.kind,
35
- flowVersion: flow.version,
36
- executionState,
37
- },
240
+ value: buildResumeEnvelope(executionState),
38
241
  };
39
242
  },
40
243
  };
244
+ function collectPublishedArtifacts(executionState) {
245
+ const merged = [];
246
+ const seen = new Set();
247
+ for (const phase of executionState.phases) {
248
+ for (const step of phase.steps) {
249
+ for (const artifact of step.publishedArtifacts ?? []) {
250
+ if (seen.has(artifact.artifact_id)) {
251
+ continue;
252
+ }
253
+ seen.add(artifact.artifact_id);
254
+ merged.push(artifact);
255
+ }
256
+ }
257
+ }
258
+ return merged;
259
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
3
4
  import { TaskRunnerError } from "../../errors.js";
4
5
  import { validateUserInputValues } from "../../user-input.js";
5
6
  function parseCommitMessage(content) {
@@ -131,6 +132,13 @@ export const gitCommitFormNode = {
131
132
  kind: "artifact",
132
133
  path: params.outputFile,
133
134
  required: true,
135
+ manifest: {
136
+ publish: true,
137
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
138
+ payloadFamily: "structured-json",
139
+ schemaId: "user-input/v1",
140
+ schemaVersion: 1,
141
+ },
134
142
  },
135
143
  ],
136
144
  };
@@ -1,5 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { readFileSync } from "node:fs";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
3
4
  import { TaskRunnerError } from "../../errors.js";
4
5
  function normalizeMarkdownLanguage(mdLang) {
5
6
  return mdLang === "en" ? "en" : "ru";
@@ -101,8 +102,24 @@ export const gitlabReviewArtifactsNode = {
101
102
  readyToMerge: artifact.ready_to_merge,
102
103
  },
103
104
  outputs: [
104
- { kind: "artifact", path: params.reviewFile, required: true },
105
- { kind: "artifact", path: params.reviewJsonFile, required: true },
105
+ {
106
+ kind: "artifact",
107
+ path: params.reviewFile,
108
+ required: true,
109
+ manifest: {
110
+ publish: true,
111
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.reviewFile),
112
+ },
113
+ },
114
+ {
115
+ kind: "artifact",
116
+ path: params.reviewJsonFile,
117
+ required: true,
118
+ manifest: {
119
+ publish: true,
120
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.reviewJsonFile),
121
+ },
122
+ },
106
123
  ],
107
124
  };
108
125
  },
@@ -10,15 +10,61 @@ export const jiraFetchNode = {
10
10
  ...(params.attachmentsManifestFile ? { attachmentsManifestFile: params.attachmentsManifestFile } : {}),
11
11
  ...(params.attachmentsContextFile ? { attachmentsContextFile: params.attachmentsContextFile } : {}),
12
12
  }, executor.defaultConfig);
13
- const outputs = [{ kind: "file", path: params.outputFile, required: true }];
13
+ const outputs = [
14
+ {
15
+ kind: "file",
16
+ path: params.outputFile,
17
+ required: true,
18
+ manifest: {
19
+ publish: true,
20
+ logicalKey: "artifacts/jira-task.json",
21
+ payloadFamily: "helper-json",
22
+ schemaId: "helper-json/v1",
23
+ schemaVersion: 1,
24
+ },
25
+ },
26
+ ];
14
27
  if (params.attachmentsManifestFile) {
15
- outputs.push({ kind: "artifact", path: params.attachmentsManifestFile, required: true });
28
+ outputs.push({
29
+ kind: "artifact",
30
+ path: params.attachmentsManifestFile,
31
+ required: true,
32
+ manifest: {
33
+ publish: true,
34
+ logicalKey: "artifacts/jira-attachments.json",
35
+ payloadFamily: "helper-json",
36
+ schemaId: "helper-json/v1",
37
+ schemaVersion: 1,
38
+ },
39
+ });
16
40
  }
17
41
  if (params.attachmentsContextFile) {
18
- outputs.push({ kind: "artifact", path: params.attachmentsContextFile, required: true });
42
+ outputs.push({
43
+ kind: "artifact",
44
+ path: params.attachmentsContextFile,
45
+ required: true,
46
+ manifest: {
47
+ publish: true,
48
+ logicalKey: "jira-attachments-context.txt",
49
+ payloadFamily: "plain-text",
50
+ schemaId: "plain-text/v1",
51
+ schemaVersion: 1,
52
+ },
53
+ });
19
54
  }
20
55
  if (value.enrichedFile) {
21
- outputs.push({ kind: "artifact", path: value.enrichedFile, required: false });
56
+ outputs.push({
57
+ kind: "artifact",
58
+ path: value.enrichedFile,
59
+ required: false,
60
+ manifest: {
61
+ publish: true,
62
+ logicalKey: "artifacts/jira-task-enriched.json",
63
+ payloadFamily: "helper-json",
64
+ schemaId: "helper-json/v1",
65
+ schemaVersion: 1,
66
+ },
67
+ });
22
68
  }
23
69
  return {
24
70
  value,
@@ -2,24 +2,44 @@ import { TaskRunnerError } from "../../errors.js";
2
2
  import { printInfo, printPrompt } from "../../tui.js";
3
3
  import { isAllowedModelForExecutor, isLlmExecutorId, } from "../launch-profile-config.js";
4
4
  import { toExecutorContext } from "../types.js";
5
+ function outputsForArtifacts(requiredArtifacts) {
6
+ return Array.from(new Set(requiredArtifacts ?? [])).map((outputPath) => ({
7
+ kind: "artifact",
8
+ path: outputPath,
9
+ required: true,
10
+ manifest: {
11
+ publish: true,
12
+ },
13
+ }));
14
+ }
5
15
  export const llmPromptNode = {
6
16
  kind: "llm-prompt",
7
17
  version: 1,
8
18
  async run(context, params) {
9
- if (!isLlmExecutorId(params.executor)) {
10
- throw new TaskRunnerError(`Unsupported llm executor '${params.executor}'.`);
19
+ const routedProfile = params.routingGroup
20
+ ? context.executionRouting?.groups[params.routingGroup]
21
+ : undefined;
22
+ const fallbackProfile = context.executionRouting?.defaultRoute;
23
+ const executor = params.routingGroup
24
+ ? routedProfile?.executor ?? params.executor ?? fallbackProfile?.executor
25
+ : params.executor ?? fallbackProfile?.executor;
26
+ const model = params.routingGroup
27
+ ? routedProfile?.model ?? params.model ?? fallbackProfile?.model
28
+ : params.model ?? fallbackProfile?.model;
29
+ if (!executor || !isLlmExecutorId(executor)) {
30
+ throw new TaskRunnerError(`Unsupported llm executor '${String(executor ?? params.executor ?? "undefined")}'.`);
11
31
  }
12
- if (params.model && !isAllowedModelForExecutor(params.executor, params.model)) {
13
- throw new TaskRunnerError(`Model '${params.model}' is not allowed for executor '${params.executor}'.`);
32
+ if (model && !isAllowedModelForExecutor(executor, model)) {
33
+ throw new TaskRunnerError(`Model '${model}' is not allowed for executor '${executor}'.`);
14
34
  }
15
35
  printInfo(params.labelText);
16
- printPrompt(`LLM:${params.executor}`, params.prompt);
36
+ printPrompt(`LLM:${executor}`, params.prompt);
17
37
  const executorContext = toExecutorContext(context);
18
- if (params.executor === "codex") {
38
+ if (executor === "codex") {
19
39
  const executor = context.executors.get("codex");
20
40
  const value = await executor.execute(executorContext, {
21
41
  prompt: params.prompt,
22
- ...(params.model ? { model: params.model } : {}),
42
+ ...(model ? { model } : {}),
23
43
  env: { ...context.env },
24
44
  }, executor.defaultConfig);
25
45
  return {
@@ -27,14 +47,14 @@ export const llmPromptNode = {
27
47
  ...value,
28
48
  executor: "codex",
29
49
  },
30
- outputs: (params.requiredArtifacts ?? []).map((path) => ({ kind: "artifact", path, required: true })),
50
+ outputs: outputsForArtifacts(params.requiredArtifacts),
31
51
  };
32
52
  }
33
- if (params.executor === "opencode") {
53
+ if (executor === "opencode") {
34
54
  const executor = context.executors.get("opencode");
35
55
  const value = await executor.execute(executorContext, {
36
56
  prompt: params.prompt,
37
- ...(params.model ? { model: params.model } : {}),
57
+ ...(model ? { model } : {}),
38
58
  env: { ...context.env },
39
59
  }, executor.defaultConfig);
40
60
  return {
@@ -42,10 +62,10 @@ export const llmPromptNode = {
42
62
  ...value,
43
63
  executor: "opencode",
44
64
  },
45
- outputs: (params.requiredArtifacts ?? []).map((path) => ({ kind: "artifact", path, required: true })),
65
+ outputs: outputsForArtifacts(params.requiredArtifacts),
46
66
  };
47
67
  }
48
- throw new TaskRunnerError(`Unsupported llm executor '${params.executor}'.`);
68
+ throw new TaskRunnerError(`Unsupported llm executor '${executor}'.`);
49
69
  },
50
70
  checks(_context, params) {
51
71
  if (!params.requiredArtifacts || params.requiredArtifacts.length === 0) {
@@ -0,0 +1,10 @@
1
+ import { resolveLatestPlanningBundle } from "../../runtime/planning-bundle.js";
2
+ export const planningBundleNode = {
3
+ kind: "planning-bundle",
4
+ version: 1,
5
+ async run(_context, params) {
6
+ return {
7
+ value: resolveLatestPlanningBundle(params.taskKey),
8
+ };
9
+ },
10
+ };
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { reviewJsonFile, latestArtifactIteration, readyToMergeFile } from "../../artifacts.js";
3
+ import { TaskRunnerError } from "../../errors.js";
4
+ import { resolveBlockingReviewSeverities, normalizeReviewSeverity } from "../../review-severity.js";
5
+ import { validateStructuredArtifacts } from "../../structured-artifacts.js";
6
+ import { clearReadyToMergeFile, writeReadyToMergeFile } from "../../runtime/ready-to-merge.js";
7
+ function readReviewVerdictFile(path) {
8
+ if (!existsSync(path)) {
9
+ throw new TaskRunnerError(`Review findings file not found: ${path}`);
10
+ }
11
+ try {
12
+ return JSON.parse(readFileSync(path, "utf8"));
13
+ }
14
+ catch {
15
+ throw new TaskRunnerError(`Failed to parse review findings JSON: ${path}`);
16
+ }
17
+ }
18
+ export const reviewVerdictNode = {
19
+ kind: "review-verdict",
20
+ version: 1,
21
+ async run(context, params) {
22
+ const iteration = params.iteration ?? latestArtifactIteration(params.taskKey, "review", "json") ?? 1;
23
+ const jsonPath = reviewJsonFile(params.taskKey, iteration);
24
+ validateStructuredArtifacts([{ path: jsonPath, schemaId: "review-findings/v1" }], "Review findings are invalid or missing.");
25
+ const report = readReviewVerdictFile(jsonPath);
26
+ const findings = Array.isArray(report.findings) ? report.findings : [];
27
+ const blockingSeverities = resolveBlockingReviewSeverities(params.blockingSeverities);
28
+ const blockingSeveritySet = new Set(blockingSeverities);
29
+ const blockingFindingTitles = findings
30
+ .filter((finding) => {
31
+ const severity = normalizeReviewSeverity(finding.severity);
32
+ return severity !== null && blockingSeveritySet.has(severity);
33
+ })
34
+ .map((finding) => (typeof finding.title === "string" ? finding.title.trim() : ""))
35
+ .filter((title) => title.length > 0);
36
+ const readyToMerge = blockingFindingTitles.length === 0;
37
+ const summary = typeof report.summary === "string" && report.summary.trim().length > 0
38
+ ? report.summary.trim()
39
+ : readyToMerge
40
+ ? "No blocking findings."
41
+ : `Blocking findings: ${blockingFindingTitles.join(", ")}`;
42
+ const normalizedReport = {
43
+ ...report,
44
+ ready_to_merge: readyToMerge,
45
+ };
46
+ writeFileSync(jsonPath, `${JSON.stringify(normalizedReport, null, 2)}\n`, "utf8");
47
+ if (readyToMerge) {
48
+ writeReadyToMergeFile(params.taskKey, {
49
+ ...(context.mdLang !== undefined ? { mdLang: context.mdLang } : {}),
50
+ summary,
51
+ });
52
+ }
53
+ else {
54
+ clearReadyToMergeFile(params.taskKey);
55
+ }
56
+ return {
57
+ value: {
58
+ readyToMerge,
59
+ blockingSeverities,
60
+ blockingFindingTitles,
61
+ blockingFindingsCount: blockingFindingTitles.length,
62
+ summary,
63
+ reviewJsonFile: jsonPath,
64
+ },
65
+ outputs: [
66
+ {
67
+ kind: "artifact",
68
+ path: jsonPath,
69
+ required: true,
70
+ manifest: {
71
+ publish: true,
72
+ schemaId: "review-findings/v1",
73
+ },
74
+ },
75
+ {
76
+ kind: "file",
77
+ path: readyToMergeFile(params.taskKey),
78
+ required: false,
79
+ manifest: {
80
+ publish: true,
81
+ },
82
+ },
83
+ ],
84
+ };
85
+ },
86
+ };
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../../artifact-manifest.js";
3
4
  import { TaskRunnerError } from "../../errors.js";
4
5
  import { validateUserInputValues } from "../../user-input.js";
5
6
  export const selectFilesFormNode = {
@@ -65,6 +66,13 @@ export const selectFilesFormNode = {
65
66
  kind: "artifact",
66
67
  path: params.outputFile,
67
68
  required: true,
69
+ manifest: {
70
+ publish: true,
71
+ logicalKey: buildLogicalKeyForPayload(context.issueKey, params.outputFile),
72
+ payloadFamily: "structured-json",
73
+ schemaId: "user-input/v1",
74
+ schemaVersion: 1,
75
+ },
68
76
  },
69
77
  ],
70
78
  };