agentweaver 0.1.19 → 0.1.20
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/README.md +47 -7
- package/dist/artifacts.js +9 -0
- package/dist/executors/git-commit-executor.js +24 -6
- package/dist/flow-state.js +3 -8
- package/dist/git/git-diff-parser.js +223 -0
- package/dist/git/git-service.js +562 -0
- package/dist/git/git-stage-selection.js +24 -0
- package/dist/git/git-status-parser.js +171 -0
- package/dist/git/git-types.js +1 -0
- package/dist/index.js +450 -108
- package/dist/interactive/auto-flow.js +644 -0
- package/dist/interactive/controller.js +417 -9
- package/dist/interactive/progress.js +194 -1
- package/dist/interactive/state.js +25 -0
- package/dist/interactive/web/index.js +97 -12
- package/dist/interactive/web/protocol.js +216 -1
- package/dist/interactive/web/server.js +72 -14
- package/dist/interactive/web/static/app.js +1603 -49
- package/dist/interactive/web/static/index.html +76 -11
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +901 -47
- package/dist/pipeline/auto-flow-blocks.js +307 -0
- package/dist/pipeline/auto-flow-config.js +273 -0
- package/dist/pipeline/auto-flow-identity.js +49 -0
- package/dist/pipeline/auto-flow-presets.js +52 -0
- package/dist/pipeline/auto-flow-resolver.js +830 -0
- package/dist/pipeline/auto-flow-types.js +17 -0
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flows.js +27 -1
- package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
- package/dist/pipeline/flow-specs/auto-golang.json +12 -1
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
- package/dist/pipeline/flow-specs/review/review-project.json +19 -1
- package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
- package/dist/pipeline/node-registry.js +9 -0
- package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
- package/dist/pipeline/nodes/flow-run-node.js +5 -3
- package/dist/pipeline/nodes/git-status-node.js +2 -168
- package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
- package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
- package/dist/pipeline/nodes/plan-codex-node.js +8 -1
- package/dist/pipeline/spec-loader.js +14 -4
- package/dist/runtime/artifact-catalog.js +29 -5
- package/dist/runtime/settings.js +114 -0
- package/dist/scope.js +14 -4
- package/package.json +1 -1
- package/dist/pipeline/flow-specs/auto-common.json +0 -179
- package/dist/pipeline/flow-specs/auto-simple.json +0 -141
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { AUTO_FLOW_BLOCK_IDS, AUTO_FLOW_SLOT_NAMES, allowedAutoFlowBlocksForSlot, validateAutoFlowBlockInsertion, } from "../pipeline/auto-flow-config.js";
|
|
2
|
+
import { getBuiltInAutoFlowBlockDefinition, listBuiltInAutoFlowBlockDefinitions } from "../pipeline/auto-flow-blocks.js";
|
|
3
|
+
import { getBuiltInAutoFlowPreset } from "../pipeline/auto-flow-presets.js";
|
|
4
|
+
import { AUTO_FLOW_SLOT_IDS, } from "../pipeline/auto-flow-types.js";
|
|
5
|
+
const SLOT_TITLES = {
|
|
6
|
+
source: "Source",
|
|
7
|
+
normalize: "Normalize",
|
|
8
|
+
planning: "Planning",
|
|
9
|
+
designReview: "Design review",
|
|
10
|
+
implementation: "Implementation",
|
|
11
|
+
postImplementationChecks: "Post-implementation checks",
|
|
12
|
+
review: "Review",
|
|
13
|
+
final: "Final",
|
|
14
|
+
};
|
|
15
|
+
const OPTIONAL_SLOT_IDS = new Set(AUTO_FLOW_SLOT_NAMES);
|
|
16
|
+
const FALLBACK_BLOCK_TITLES = {
|
|
17
|
+
"review.design-loop": "Design review loop",
|
|
18
|
+
"checks.go.linter": "Go linter loop",
|
|
19
|
+
"checks.go.tests": "Go tests loop",
|
|
20
|
+
"review.loop": "Review loop",
|
|
21
|
+
};
|
|
22
|
+
const FALLBACK_MAX_ITERATION_DEFAULTS = {
|
|
23
|
+
"review.design-loop": 3,
|
|
24
|
+
"checks.go.linter": 5,
|
|
25
|
+
"checks.go.tests": 5,
|
|
26
|
+
"review.loop": 5,
|
|
27
|
+
};
|
|
28
|
+
function cloneJson(value) {
|
|
29
|
+
return JSON.parse(JSON.stringify(value));
|
|
30
|
+
}
|
|
31
|
+
function isOptionalSlot(slotId) {
|
|
32
|
+
return OPTIONAL_SLOT_IDS.has(slotId);
|
|
33
|
+
}
|
|
34
|
+
function isSavedBlockId(blockId) {
|
|
35
|
+
return AUTO_FLOW_BLOCK_IDS.includes(blockId);
|
|
36
|
+
}
|
|
37
|
+
function optionalSlotForBlock(blockId) {
|
|
38
|
+
for (const slotName of AUTO_FLOW_SLOT_NAMES) {
|
|
39
|
+
if (allowedAutoFlowBlocksForSlot(slotName).includes(blockId)) {
|
|
40
|
+
return slotName;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function validationDiagnostic(validation, fallback) {
|
|
46
|
+
return {
|
|
47
|
+
code: validation.message.startsWith("Unknown auto-flow block") ? "unknown-block" : "invalid-slot",
|
|
48
|
+
message: validation.message,
|
|
49
|
+
...(validation.blockId ? { blockId: validation.blockId } : { blockId: fallback.blockId }),
|
|
50
|
+
...(validation.slotName ? { slotId: validation.slotName } : fallback.slotId ? { slotId: fallback.slotId } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function resolveOptionalBlockSlot(blockId, slotId) {
|
|
54
|
+
if (slotId !== undefined) {
|
|
55
|
+
const validation = validateAutoFlowBlockInsertion(slotId, blockId);
|
|
56
|
+
if (!validation.ok || !validation.slotName || !validation.blockId) {
|
|
57
|
+
return {
|
|
58
|
+
diagnostics: [validationDiagnostic(validation, { slotId, blockId })],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
slotName: validation.slotName,
|
|
63
|
+
blockId: validation.blockId,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const slotName = optionalSlotForBlock(blockId);
|
|
67
|
+
if (!slotName || !isSavedBlockId(blockId)) {
|
|
68
|
+
return {
|
|
69
|
+
diagnostics: [{
|
|
70
|
+
code: "unknown-block",
|
|
71
|
+
message: `Unknown optional auto-flow block '${blockId}'.`,
|
|
72
|
+
blockId,
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
slotName,
|
|
78
|
+
blockId,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function defaultOptionalBlocksForPreset(presetId, slotId) {
|
|
82
|
+
const preset = getBuiltInAutoFlowPreset(presetId);
|
|
83
|
+
return preset.blocks
|
|
84
|
+
.filter((placement) => placement.slot === slotId && isSavedBlockId(placement.blockId))
|
|
85
|
+
.map((placement) => ({
|
|
86
|
+
id: placement.blockId,
|
|
87
|
+
enabled: placement.defaultEnabled === false ? false : "auto",
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function coreBlockForSlot(presetId, slotId, diagnostics) {
|
|
91
|
+
const preset = getBuiltInAutoFlowPreset(presetId);
|
|
92
|
+
const placement = preset.blocks.find((candidate) => candidate.slot === slotId && candidate.locked);
|
|
93
|
+
if (!placement) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
97
|
+
const phaseId = phaseIdForBlock(slotId, placement.blockId);
|
|
98
|
+
const blockDiagnostics = diagnosticsFor(diagnostics, { slotId, blockId: placement.blockId });
|
|
99
|
+
const status = blockDiagnostics.some((diagnostic) => (diagnostic.code === "locked-block-disabled"
|
|
100
|
+
|| diagnostic.code === "locked-block-removed"
|
|
101
|
+
|| diagnostic.code === "missing-dependency"))
|
|
102
|
+
? "blocked"
|
|
103
|
+
: blockDiagnostics.length > 0 ? "invalid" : "pending";
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
blockId: placement.blockId,
|
|
107
|
+
title: definition?.title ?? placement.blockId,
|
|
108
|
+
slotId,
|
|
109
|
+
status,
|
|
110
|
+
reason: blockDiagnostics[0]?.message ?? "Locked core block.",
|
|
111
|
+
locked: true,
|
|
112
|
+
enabled: true,
|
|
113
|
+
actions: {
|
|
114
|
+
canEnable: false,
|
|
115
|
+
canDisable: false,
|
|
116
|
+
canRemove: false,
|
|
117
|
+
canEditParams: false,
|
|
118
|
+
},
|
|
119
|
+
params: [],
|
|
120
|
+
diagnostics: blockDiagnostics,
|
|
121
|
+
...(phaseId ? { phaseId } : {}),
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function blockTitle(blockId) {
|
|
126
|
+
return getBuiltInAutoFlowBlockDefinition(blockId)?.title
|
|
127
|
+
?? (isSavedBlockId(blockId) ? FALLBACK_BLOCK_TITLES[blockId] : undefined)
|
|
128
|
+
?? blockId;
|
|
129
|
+
}
|
|
130
|
+
function parameterDefinitionForBlock(blockId) {
|
|
131
|
+
const definition = getBuiltInAutoFlowBlockDefinition(blockId);
|
|
132
|
+
const builtInParam = definition?.params?.maxIterations;
|
|
133
|
+
if (builtInParam) {
|
|
134
|
+
return builtInParam;
|
|
135
|
+
}
|
|
136
|
+
const fallbackDefault = FALLBACK_MAX_ITERATION_DEFAULTS[blockId];
|
|
137
|
+
if (fallbackDefault === undefined) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type: "integer",
|
|
142
|
+
min: 1,
|
|
143
|
+
max: 5,
|
|
144
|
+
default: fallbackDefault,
|
|
145
|
+
supportedExecutableValues: [fallbackDefault],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function blockParams(block) {
|
|
149
|
+
const definition = parameterDefinitionForBlock(block.id);
|
|
150
|
+
if (!definition) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
name: "maxIterations",
|
|
156
|
+
label: "maxIterations",
|
|
157
|
+
type: "integer",
|
|
158
|
+
value: block.maxIterations ?? definition.default,
|
|
159
|
+
defaultValue: definition.default,
|
|
160
|
+
min: definition.min,
|
|
161
|
+
max: definition.max,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
function phaseIdForBlock(slotId, blockId) {
|
|
166
|
+
if (blockId === "source.jira")
|
|
167
|
+
return "source";
|
|
168
|
+
if (blockId === "normalize.task-source")
|
|
169
|
+
return "normalize";
|
|
170
|
+
if (blockId === "planning.plan")
|
|
171
|
+
return "plan";
|
|
172
|
+
if (blockId === "review.design-loop")
|
|
173
|
+
return "design_review_loop";
|
|
174
|
+
if (blockId === "implementation.default")
|
|
175
|
+
return "implement";
|
|
176
|
+
if (blockId === "review.loop")
|
|
177
|
+
return "review-loop";
|
|
178
|
+
if (blockId === "checks.go.linter")
|
|
179
|
+
return slotId === "final" ? "final_go_linter_loop" : "post_go_linter_loop";
|
|
180
|
+
if (blockId === "checks.go.tests")
|
|
181
|
+
return slotId === "final" ? "final_go_tests_loop" : "post_go_tests_loop";
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function diagnosticsFor(diagnostics, input) {
|
|
185
|
+
return diagnostics.filter((diagnostic) => {
|
|
186
|
+
if (input.slotId && diagnostic.slotId !== undefined && diagnostic.slotId !== input.slotId) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (input.blockId && diagnostic.blockId !== input.blockId) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (input.paramName && diagnostic.paramName !== input.paramName) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function validateEditorConfig(config) {
|
|
199
|
+
const diagnostics = [];
|
|
200
|
+
for (const slotName of AUTO_FLOW_SLOT_NAMES) {
|
|
201
|
+
const slot = config.slots?.[slotName];
|
|
202
|
+
if (!slot) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
slot.blocks.forEach((block, index) => {
|
|
207
|
+
if (!allowedAutoFlowBlocksForSlot(slotName).includes(block.id)) {
|
|
208
|
+
diagnostics.push({
|
|
209
|
+
code: "invalid-slot",
|
|
210
|
+
message: `Auto-flow block '${block.id}' cannot be placed in slot '${slotName}'. Allowed blocks: ${allowedAutoFlowBlocksForSlot(slotName).join(", ")}.`,
|
|
211
|
+
blockId: block.id,
|
|
212
|
+
slotId: slotName,
|
|
213
|
+
allowedSlots: [slotName],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (seen.has(block.id)) {
|
|
217
|
+
diagnostics.push({
|
|
218
|
+
code: "duplicate-block",
|
|
219
|
+
message: `Auto-flow block '${block.id}' is placed more than once in slot '${slotName}'.`,
|
|
220
|
+
blockId: block.id,
|
|
221
|
+
slotId: slotName,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
seen.add(block.id);
|
|
225
|
+
const paramDefinition = parameterDefinitionForBlock(block.id);
|
|
226
|
+
if (block.maxIterations !== undefined && !paramDefinition) {
|
|
227
|
+
diagnostics.push({
|
|
228
|
+
code: "unknown-parameter",
|
|
229
|
+
message: `Auto-flow block '${block.id}' does not support parameter 'maxIterations'.`,
|
|
230
|
+
blockId: block.id,
|
|
231
|
+
slotId: slotName,
|
|
232
|
+
paramName: "maxIterations",
|
|
233
|
+
value: block.maxIterations,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (block.maxIterations !== undefined && paramDefinition && !Number.isInteger(block.maxIterations)) {
|
|
237
|
+
diagnostics.push({
|
|
238
|
+
code: "invalid-parameter-type",
|
|
239
|
+
message: `Auto-flow block '${block.id}' parameter 'maxIterations' must be an integer; received ${JSON.stringify(block.maxIterations)}.`,
|
|
240
|
+
blockId: block.id,
|
|
241
|
+
slotId: slotName,
|
|
242
|
+
paramName: "maxIterations",
|
|
243
|
+
value: block.maxIterations,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (block.maxIterations !== undefined
|
|
247
|
+
&& paramDefinition
|
|
248
|
+
&& Number.isInteger(block.maxIterations)
|
|
249
|
+
&& (block.maxIterations < paramDefinition.min || block.maxIterations > paramDefinition.max)) {
|
|
250
|
+
diagnostics.push({
|
|
251
|
+
code: "parameter-out-of-range",
|
|
252
|
+
message: `Auto-flow block '${block.id}' parameter 'maxIterations' must be between ${paramDefinition.min} and ${paramDefinition.max}; received ${block.maxIterations}.`,
|
|
253
|
+
blockId: block.id,
|
|
254
|
+
slotId: slotName,
|
|
255
|
+
paramName: "maxIterations",
|
|
256
|
+
value: block.maxIterations,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (!isSavedBlockId(block.id)) {
|
|
260
|
+
diagnostics.push({
|
|
261
|
+
code: "unknown-block",
|
|
262
|
+
message: `Unknown auto-flow block '${block.id}' at slots.${slotName}.blocks[${index}].`,
|
|
263
|
+
blockId: block.id,
|
|
264
|
+
slotId: slotName,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return diagnostics;
|
|
270
|
+
}
|
|
271
|
+
function optionalBlocksForSlot(config, slotId) {
|
|
272
|
+
const defaultBlocks = defaultOptionalBlocksForPreset(config.basePreset, slotId);
|
|
273
|
+
const configuredBlocks = config.slots?.[slotId]?.blocks;
|
|
274
|
+
if (!configuredBlocks) {
|
|
275
|
+
return defaultBlocks.map((block) => ({
|
|
276
|
+
block,
|
|
277
|
+
statusReason: "Included by preset default.",
|
|
278
|
+
canRemove: true,
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
const configuredIds = new Set(configuredBlocks.map((block) => block.id));
|
|
282
|
+
const blocks = configuredBlocks.map((block) => ({
|
|
283
|
+
block,
|
|
284
|
+
statusReason: "Configured in saved auto-flow config.",
|
|
285
|
+
canRemove: true,
|
|
286
|
+
}));
|
|
287
|
+
for (const defaultBlock of defaultBlocks) {
|
|
288
|
+
if (configuredIds.has(defaultBlock.id)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
blocks.push({
|
|
292
|
+
block: {
|
|
293
|
+
...defaultBlock,
|
|
294
|
+
enabled: false,
|
|
295
|
+
},
|
|
296
|
+
statusReason: "Skipped because the slot override omitted this preset default block.",
|
|
297
|
+
canRemove: false,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return blocks;
|
|
301
|
+
}
|
|
302
|
+
function blockStatus(block, diagnostics) {
|
|
303
|
+
if (diagnostics.length > 0) {
|
|
304
|
+
if (diagnostics.some((diagnostic) => (diagnostic.code === "locked-block-disabled"
|
|
305
|
+
|| diagnostic.code === "locked-block-removed"
|
|
306
|
+
|| diagnostic.code === "missing-dependency"))) {
|
|
307
|
+
return "blocked";
|
|
308
|
+
}
|
|
309
|
+
return "invalid";
|
|
310
|
+
}
|
|
311
|
+
if (block.enabled === false) {
|
|
312
|
+
return "disabled";
|
|
313
|
+
}
|
|
314
|
+
return "pending";
|
|
315
|
+
}
|
|
316
|
+
function blockReason(block, statusReason, diagnostics) {
|
|
317
|
+
if (diagnostics[0]) {
|
|
318
|
+
return diagnostics[0].message;
|
|
319
|
+
}
|
|
320
|
+
if (block.enabled === false) {
|
|
321
|
+
return "Optional block is disabled.";
|
|
322
|
+
}
|
|
323
|
+
return statusReason;
|
|
324
|
+
}
|
|
325
|
+
function optionalBlockView(slotId, block, statusReason, canRemove, diagnostics) {
|
|
326
|
+
const blockDiagnostics = diagnosticsFor(diagnostics, { slotId, blockId: block.id });
|
|
327
|
+
const params = blockParams(block);
|
|
328
|
+
const phaseId = phaseIdForBlock(slotId, block.id);
|
|
329
|
+
return {
|
|
330
|
+
blockId: block.id,
|
|
331
|
+
title: blockTitle(block.id),
|
|
332
|
+
slotId,
|
|
333
|
+
status: blockStatus(block, blockDiagnostics),
|
|
334
|
+
reason: blockReason(block, statusReason, blockDiagnostics),
|
|
335
|
+
locked: false,
|
|
336
|
+
enabled: block.enabled !== false,
|
|
337
|
+
actions: {
|
|
338
|
+
canEnable: block.enabled === false,
|
|
339
|
+
canDisable: block.enabled !== false,
|
|
340
|
+
canRemove,
|
|
341
|
+
canEditParams: params.length > 0,
|
|
342
|
+
},
|
|
343
|
+
params,
|
|
344
|
+
diagnostics: blockDiagnostics,
|
|
345
|
+
...(phaseId ? { phaseId } : {}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function slotStatus(blocks, diagnostics) {
|
|
349
|
+
if (diagnostics.length > 0 || blocks.some((block) => block.status === "invalid")) {
|
|
350
|
+
return "invalid";
|
|
351
|
+
}
|
|
352
|
+
if (blocks.length === 0) {
|
|
353
|
+
return "empty";
|
|
354
|
+
}
|
|
355
|
+
if (blocks.every((block) => block.status === "disabled")) {
|
|
356
|
+
return "disabled";
|
|
357
|
+
}
|
|
358
|
+
return "pending";
|
|
359
|
+
}
|
|
360
|
+
function slotReason(blocks, diagnostics) {
|
|
361
|
+
if (diagnostics[0]) {
|
|
362
|
+
return diagnostics[0].message;
|
|
363
|
+
}
|
|
364
|
+
if (blocks.length === 0) {
|
|
365
|
+
return "No blocks are configured for this optional slot.";
|
|
366
|
+
}
|
|
367
|
+
if (blocks.every((block) => block.status === "disabled")) {
|
|
368
|
+
return "All optional blocks in this slot are disabled.";
|
|
369
|
+
}
|
|
370
|
+
return "Slot is configured.";
|
|
371
|
+
}
|
|
372
|
+
function slotView(config, slotId, diagnostics) {
|
|
373
|
+
const slotDiagnostics = diagnosticsFor(diagnostics, { slotId });
|
|
374
|
+
const blocks = isOptionalSlot(slotId)
|
|
375
|
+
? optionalBlocksForSlot(config, slotId).map(({ block, statusReason, canRemove }) => optionalBlockView(slotId, block, statusReason, canRemove, diagnostics))
|
|
376
|
+
: coreBlockForSlot(config.basePreset, slotId, diagnostics);
|
|
377
|
+
return {
|
|
378
|
+
slotId,
|
|
379
|
+
title: SLOT_TITLES[slotId],
|
|
380
|
+
status: slotStatus(blocks, slotDiagnostics),
|
|
381
|
+
reason: slotReason(blocks, slotDiagnostics),
|
|
382
|
+
blocks,
|
|
383
|
+
diagnostics: slotDiagnostics,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function sourceLabel(source) {
|
|
387
|
+
if (source.type === "preset") {
|
|
388
|
+
return `preset ${source.preset}`;
|
|
389
|
+
}
|
|
390
|
+
return `${source.type} ${source.configName}`;
|
|
391
|
+
}
|
|
392
|
+
export function defaultAutoFlowConfigForPreset(presetId, name = `preset-${presetId}`) {
|
|
393
|
+
return {
|
|
394
|
+
kind: "auto-flow-config",
|
|
395
|
+
version: 1,
|
|
396
|
+
name,
|
|
397
|
+
basePreset: presetId,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
export function createPresetAutoFlowDefinition(presetId) {
|
|
401
|
+
return {
|
|
402
|
+
selection: { kind: "preset", preset: presetId },
|
|
403
|
+
basePreset: presetId,
|
|
404
|
+
config: defaultAutoFlowConfigForPreset(presetId),
|
|
405
|
+
source: {
|
|
406
|
+
type: "preset",
|
|
407
|
+
preset: presetId,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export function createConfigAutoFlowDefinition(input) {
|
|
412
|
+
return {
|
|
413
|
+
selection: { kind: "config", name: input.config.name },
|
|
414
|
+
basePreset: input.config.basePreset,
|
|
415
|
+
config: cloneJson(input.config),
|
|
416
|
+
source: input.source,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export function buildAutoFlowEditorViewModel(definition, options = {}) {
|
|
420
|
+
const config = options.config ?? definition.config;
|
|
421
|
+
const diagnostics = options.diagnostics ?? validateEditorConfig(config);
|
|
422
|
+
const slots = AUTO_FLOW_SLOT_IDS.map((slotId) => slotView(config, slotId, diagnostics));
|
|
423
|
+
const availableBlocks = [
|
|
424
|
+
...listBuiltInAutoFlowBlockDefinitions().filter((block) => !block.locked).map((block) => ({
|
|
425
|
+
blockId: block.id,
|
|
426
|
+
title: block.title,
|
|
427
|
+
allowedSlots: [...block.allowedSlots],
|
|
428
|
+
})),
|
|
429
|
+
{
|
|
430
|
+
blockId: "checks.go.linter",
|
|
431
|
+
title: "Go linter loop",
|
|
432
|
+
allowedSlots: ["postImplementationChecks", "final"],
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
blockId: "checks.go.tests",
|
|
436
|
+
title: "Go tests loop",
|
|
437
|
+
allowedSlots: ["postImplementationChecks", "final"],
|
|
438
|
+
},
|
|
439
|
+
];
|
|
440
|
+
const valid = diagnostics.length === 0;
|
|
441
|
+
const canReset = JSON.stringify(config) !== JSON.stringify(definition.config);
|
|
442
|
+
return {
|
|
443
|
+
selection: definition.selection,
|
|
444
|
+
basePreset: config.basePreset,
|
|
445
|
+
configName: config.name,
|
|
446
|
+
source: definition.source,
|
|
447
|
+
slots,
|
|
448
|
+
diagnostics: [...diagnostics],
|
|
449
|
+
availableBlocks,
|
|
450
|
+
status: {
|
|
451
|
+
valid,
|
|
452
|
+
canSave: valid,
|
|
453
|
+
canReset,
|
|
454
|
+
canRun: valid,
|
|
455
|
+
saveTarget: options.saveTarget ?? "project",
|
|
456
|
+
sourceLabel: sourceLabel(definition.source),
|
|
457
|
+
...(options.lastMessage ? { lastMessage: options.lastMessage } : {}),
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
export function autoFlowSelectionForFlowId(flowId) {
|
|
462
|
+
if (flowId === "auto-simple") {
|
|
463
|
+
return { kind: "preset", preset: "simple" };
|
|
464
|
+
}
|
|
465
|
+
if (flowId === "auto-common") {
|
|
466
|
+
return { kind: "preset", preset: "standard" };
|
|
467
|
+
}
|
|
468
|
+
if (flowId.startsWith("auto-config:")) {
|
|
469
|
+
const name = flowId.slice("auto-config:".length);
|
|
470
|
+
if (/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
471
|
+
return { kind: "config", name };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
function ensureOptionalSlot(config, slotName) {
|
|
477
|
+
const nextSlots = {
|
|
478
|
+
...(config.slots ?? {}),
|
|
479
|
+
};
|
|
480
|
+
const existing = nextSlots[slotName]?.blocks;
|
|
481
|
+
const blocks = existing
|
|
482
|
+
? existing.map((block) => ({ ...block }))
|
|
483
|
+
: defaultOptionalBlocksForPreset(config.basePreset, slotName);
|
|
484
|
+
nextSlots[slotName] = { blocks };
|
|
485
|
+
config.slots = nextSlots;
|
|
486
|
+
return blocks;
|
|
487
|
+
}
|
|
488
|
+
export function setAutoFlowBlockEnabled(config, blockId, enabled, slotId) {
|
|
489
|
+
const next = cloneJson(config);
|
|
490
|
+
const coreDefinition = getBuiltInAutoFlowBlockDefinition(blockId);
|
|
491
|
+
if (coreDefinition?.locked) {
|
|
492
|
+
return {
|
|
493
|
+
config: next,
|
|
494
|
+
diagnostics: [{
|
|
495
|
+
code: "locked-block-disabled",
|
|
496
|
+
message: `Locked auto-flow block '${blockId}' cannot be disabled.`,
|
|
497
|
+
blockId,
|
|
498
|
+
}],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const resolved = resolveOptionalBlockSlot(blockId, slotId);
|
|
502
|
+
if ("diagnostics" in resolved) {
|
|
503
|
+
return {
|
|
504
|
+
config: next,
|
|
505
|
+
diagnostics: resolved.diagnostics,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const blocks = ensureOptionalSlot(next, resolved.slotName);
|
|
509
|
+
const existing = blocks.find((block) => block.id === resolved.blockId);
|
|
510
|
+
if (existing) {
|
|
511
|
+
existing.enabled = enabled ? true : false;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
blocks.push({
|
|
515
|
+
id: resolved.blockId,
|
|
516
|
+
enabled: enabled ? true : false,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
config: next,
|
|
521
|
+
diagnostics: validateEditorConfig(next),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
export function updateAutoFlowBlockParameter(config, blockId, paramName, value, slotId) {
|
|
525
|
+
const next = cloneJson(config);
|
|
526
|
+
if (paramName !== "maxIterations") {
|
|
527
|
+
return {
|
|
528
|
+
config: next,
|
|
529
|
+
diagnostics: [{
|
|
530
|
+
code: "unknown-parameter",
|
|
531
|
+
message: `Auto-flow block '${blockId}' does not support parameter '${paramName}'.`,
|
|
532
|
+
blockId,
|
|
533
|
+
paramName,
|
|
534
|
+
value,
|
|
535
|
+
}],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const resolved = resolveOptionalBlockSlot(blockId, slotId);
|
|
539
|
+
if ("diagnostics" in resolved) {
|
|
540
|
+
return {
|
|
541
|
+
config: next,
|
|
542
|
+
diagnostics: resolved.diagnostics.map((diagnostic) => ({ ...diagnostic, paramName, value })),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const blocks = ensureOptionalSlot(next, resolved.slotName);
|
|
546
|
+
let block = blocks.find((candidate) => candidate.id === resolved.blockId);
|
|
547
|
+
if (!block) {
|
|
548
|
+
block = {
|
|
549
|
+
id: resolved.blockId,
|
|
550
|
+
enabled: true,
|
|
551
|
+
};
|
|
552
|
+
blocks.push(block);
|
|
553
|
+
}
|
|
554
|
+
block.maxIterations = value;
|
|
555
|
+
return {
|
|
556
|
+
config: next,
|
|
557
|
+
diagnostics: validateEditorConfig(next),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
export function insertAutoFlowBlock(config, slotId, blockId) {
|
|
561
|
+
const next = cloneJson(config);
|
|
562
|
+
const validation = validateAutoFlowBlockInsertion(slotId, blockId);
|
|
563
|
+
if (!validation.ok) {
|
|
564
|
+
return {
|
|
565
|
+
config: next,
|
|
566
|
+
inserted: false,
|
|
567
|
+
diagnostics: [{
|
|
568
|
+
code: validation.message.startsWith("Unknown auto-flow block") ? "unknown-block" : "invalid-slot",
|
|
569
|
+
message: validation.message,
|
|
570
|
+
...(validation.blockId ? { blockId: validation.blockId } : { blockId }),
|
|
571
|
+
...(validation.slotName ? { slotId: validation.slotName } : { slotId }),
|
|
572
|
+
}],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (!validation.slotName || !validation.blockId) {
|
|
576
|
+
return {
|
|
577
|
+
config: next,
|
|
578
|
+
inserted: false,
|
|
579
|
+
diagnostics: [{
|
|
580
|
+
code: "invalid-slot",
|
|
581
|
+
message: validation.message,
|
|
582
|
+
blockId,
|
|
583
|
+
slotId,
|
|
584
|
+
}],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const blocks = ensureOptionalSlot(next, validation.slotName);
|
|
588
|
+
if (!blocks.some((block) => block.id === validation.blockId)) {
|
|
589
|
+
blocks.push({
|
|
590
|
+
id: validation.blockId,
|
|
591
|
+
enabled: true,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
config: next,
|
|
596
|
+
inserted: true,
|
|
597
|
+
diagnostics: validateEditorConfig(next),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
export function removeAutoFlowBlock(config, slotId, blockId) {
|
|
601
|
+
const next = cloneJson(config);
|
|
602
|
+
const coreDefinition = getBuiltInAutoFlowBlockDefinition(blockId);
|
|
603
|
+
if (coreDefinition?.locked) {
|
|
604
|
+
return {
|
|
605
|
+
config: next,
|
|
606
|
+
removed: false,
|
|
607
|
+
diagnostics: [{
|
|
608
|
+
code: "locked-block-removed",
|
|
609
|
+
message: `Locked auto-flow block '${blockId}' cannot be removed.`,
|
|
610
|
+
blockId,
|
|
611
|
+
slotId,
|
|
612
|
+
}],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const resolved = resolveOptionalBlockSlot(blockId, slotId);
|
|
616
|
+
if ("diagnostics" in resolved) {
|
|
617
|
+
return {
|
|
618
|
+
config: next,
|
|
619
|
+
removed: false,
|
|
620
|
+
diagnostics: resolved.diagnostics,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const blocks = ensureOptionalSlot(next, resolved.slotName);
|
|
624
|
+
const retained = blocks.filter((block) => block.id !== resolved.blockId);
|
|
625
|
+
const removed = retained.length !== blocks.length;
|
|
626
|
+
if (!removed) {
|
|
627
|
+
return {
|
|
628
|
+
config: next,
|
|
629
|
+
removed: false,
|
|
630
|
+
diagnostics: [{
|
|
631
|
+
code: "unknown-block",
|
|
632
|
+
message: `Auto-flow block '${resolved.blockId}' is not configured in slot '${resolved.slotName}'.`,
|
|
633
|
+
blockId: resolved.blockId,
|
|
634
|
+
slotId: resolved.slotName,
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
blocks.splice(0, blocks.length, ...retained);
|
|
639
|
+
return {
|
|
640
|
+
config: next,
|
|
641
|
+
removed: true,
|
|
642
|
+
diagnostics: validateEditorConfig(next),
|
|
643
|
+
};
|
|
644
|
+
}
|