agentweaver 0.1.18 → 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 +54 -6
- 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 +454 -108
- package/dist/interactive/auto-flow.js +644 -0
- package/dist/interactive/controller.js +489 -7
- package/dist/interactive/progress.js +194 -1
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/web/index.js +237 -5
- package/dist/interactive/web/protocol.js +222 -1
- package/dist/interactive/web/server.js +497 -3
- package/dist/interactive/web/static/app.js +2462 -37
- package/dist/interactive/web/static/index.html +113 -11
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +1383 -149
- 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 +403 -0
- 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,830 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { flowConfigYamlFile, resolvedFlowJsonFile, resolvedFlowSummaryJsonFile, } from "../artifacts.js";
|
|
5
|
+
import { TaskRunnerError } from "../errors.js";
|
|
6
|
+
import { getBuiltInAutoFlowBlockDefinition } from "./auto-flow-blocks.js";
|
|
7
|
+
import { getBuiltInAutoFlowPreset, getBuiltInAutoFlowPresetByFileName, } from "./auto-flow-presets.js";
|
|
8
|
+
import { AUTO_FLOW_SLOT_NAMES, loadAutoFlowConfigByName, normalizeAutoFlowConfigYaml, } from "./auto-flow-config.js";
|
|
9
|
+
import { AUTO_FLOW_SLOT_IDS, } from "./auto-flow-types.js";
|
|
10
|
+
import { loadDeclarativeFlowFromSpec, resolveNamedDeclarativeFlowRef, } from "./declarative-flows.js";
|
|
11
|
+
import { loadBuiltInFlowSpecSync, loadFlowSpecSync } from "./spec-loader.js";
|
|
12
|
+
const BLOCK_REGISTRY = {
|
|
13
|
+
"review.design-loop": {
|
|
14
|
+
label: "design-review loop",
|
|
15
|
+
defaultMaxIterations: 3,
|
|
16
|
+
builtInFlowFileName: "design-review-loop.json",
|
|
17
|
+
builtInSpecName: "design-review-loop.json",
|
|
18
|
+
},
|
|
19
|
+
"checks.go.linter": {
|
|
20
|
+
label: "Go linter loop",
|
|
21
|
+
defaultMaxIterations: 5,
|
|
22
|
+
builtInFlowFileName: "run-go-linter-loop.json",
|
|
23
|
+
builtInSpecName: "run-go-linter-loop.json",
|
|
24
|
+
},
|
|
25
|
+
"checks.go.tests": {
|
|
26
|
+
label: "Go tests loop",
|
|
27
|
+
defaultMaxIterations: 5,
|
|
28
|
+
builtInFlowFileName: "run-go-tests-loop.json",
|
|
29
|
+
builtInSpecName: "run-go-tests-loop.json",
|
|
30
|
+
},
|
|
31
|
+
"review.loop": {
|
|
32
|
+
label: "review loop",
|
|
33
|
+
defaultMaxIterations: 5,
|
|
34
|
+
builtInFlowFileName: "review-loop.json",
|
|
35
|
+
builtInSpecName: "review-loop.json",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const DEFAULT_BLOCKS = {
|
|
39
|
+
simple: {
|
|
40
|
+
designReview: [],
|
|
41
|
+
postImplementationChecks: [],
|
|
42
|
+
review: ["review.loop"],
|
|
43
|
+
final: [],
|
|
44
|
+
},
|
|
45
|
+
standard: {
|
|
46
|
+
designReview: ["review.design-loop"],
|
|
47
|
+
postImplementationChecks: [],
|
|
48
|
+
review: ["review.loop"],
|
|
49
|
+
final: [],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const slotOrder = new Map(AUTO_FLOW_SLOT_IDS.map((slot, index) => [slot, index]));
|
|
53
|
+
function selectedCommandForPreset(preset) {
|
|
54
|
+
return preset === "standard" ? "auto-common" : "auto-simple";
|
|
55
|
+
}
|
|
56
|
+
function builtInSpecFileForPreset(preset) {
|
|
57
|
+
return preset === "standard" ? "auto-common.json" : "auto-simple.json";
|
|
58
|
+
}
|
|
59
|
+
function cloneJson(value) {
|
|
60
|
+
return JSON.parse(JSON.stringify(value));
|
|
61
|
+
}
|
|
62
|
+
function stableJson(value) {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
65
|
+
}
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
return `{${Object.entries(value)
|
|
68
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
69
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
70
|
+
.join(",")}}`;
|
|
71
|
+
}
|
|
72
|
+
return JSON.stringify(value);
|
|
73
|
+
}
|
|
74
|
+
function isAutoFlowSlotId(value) {
|
|
75
|
+
return AUTO_FLOW_SLOT_IDS.includes(value);
|
|
76
|
+
}
|
|
77
|
+
function createSummary(presetId) {
|
|
78
|
+
return {
|
|
79
|
+
presetId,
|
|
80
|
+
enabled: [],
|
|
81
|
+
disabled: [],
|
|
82
|
+
skipped: [],
|
|
83
|
+
autoDisabled: [],
|
|
84
|
+
invalid: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function addDiagnostic(diagnostics, code, message, details = {}) {
|
|
88
|
+
diagnostics.push({ code, message, ...details });
|
|
89
|
+
}
|
|
90
|
+
function decisionFromDiagnostic(diagnostic) {
|
|
91
|
+
const decision = {
|
|
92
|
+
status: "invalid",
|
|
93
|
+
blockId: diagnostic.blockId ?? "flow",
|
|
94
|
+
reason: diagnostic.message,
|
|
95
|
+
diagnosticCode: diagnostic.code,
|
|
96
|
+
};
|
|
97
|
+
if (diagnostic.slotId && isAutoFlowSlotId(diagnostic.slotId)) {
|
|
98
|
+
decision.slotId = diagnostic.slotId;
|
|
99
|
+
}
|
|
100
|
+
return decision;
|
|
101
|
+
}
|
|
102
|
+
function defaultParamsForPlacement(placement) {
|
|
103
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
104
|
+
const params = {};
|
|
105
|
+
if (definition?.params) {
|
|
106
|
+
for (const [name, paramDefinition] of Object.entries(definition.params)) {
|
|
107
|
+
params[name] = paramDefinition.default;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...params,
|
|
112
|
+
...(placement.params ?? {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function workingPlacementsForPreset(preset, override) {
|
|
116
|
+
const sourcePlacements = override?.placements ?? preset.blocks;
|
|
117
|
+
return sourcePlacements.map((placement, index) => {
|
|
118
|
+
const presetPlacement = preset.blocks.find((candidate) => candidate.blockId === placement.blockId);
|
|
119
|
+
const defaultParams = presetPlacement ? defaultParamsForPlacement(presetPlacement) : {};
|
|
120
|
+
return {
|
|
121
|
+
blockId: placement.blockId,
|
|
122
|
+
slot: placement.slot,
|
|
123
|
+
enabled: "enabled" in placement ? placement.enabled !== false : true,
|
|
124
|
+
params: {
|
|
125
|
+
...defaultParams,
|
|
126
|
+
...(placement.params ?? {}),
|
|
127
|
+
...(override?.blockParams?.[placement.blockId] ?? {}),
|
|
128
|
+
},
|
|
129
|
+
index,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function sortedPlacements(placements) {
|
|
134
|
+
return [...placements].sort((left, right) => {
|
|
135
|
+
const leftSlot = slotOrder.get(left.slot) ?? Number.MAX_SAFE_INTEGER;
|
|
136
|
+
const rightSlot = slotOrder.get(right.slot) ?? Number.MAX_SAFE_INTEGER;
|
|
137
|
+
if (leftSlot !== rightSlot) {
|
|
138
|
+
return leftSlot - rightSlot;
|
|
139
|
+
}
|
|
140
|
+
return left.index - right.index;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function validateDisabledBlocks(preset, placements, disabledBlocks, summary, diagnostics) {
|
|
144
|
+
const activePlacements = [];
|
|
145
|
+
for (const placement of placements) {
|
|
146
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
147
|
+
if (!definition) {
|
|
148
|
+
addDiagnostic(diagnostics, "unknown-block", `Unknown auto-flow block '${placement.blockId}'.`, { blockId: placement.blockId });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const disabled = disabledBlocks.has(placement.blockId) || !placement.enabled;
|
|
152
|
+
if (!disabled) {
|
|
153
|
+
activePlacements.push(placement);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (definition.locked || preset.blocks.some((candidate) => candidate.blockId === placement.blockId && candidate.locked)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
summary.disabled.push({
|
|
160
|
+
status: "disabled",
|
|
161
|
+
blockId: placement.blockId,
|
|
162
|
+
...(isAutoFlowSlotId(placement.slot) ? { slotId: placement.slot } : {}),
|
|
163
|
+
reason: `Optional block '${placement.blockId}' was explicitly disabled.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
for (const blockId of disabledBlocks) {
|
|
167
|
+
const definition = getBuiltInAutoFlowBlockDefinition(blockId);
|
|
168
|
+
if (!definition) {
|
|
169
|
+
addDiagnostic(diagnostics, "unknown-block", `Unknown auto-flow block '${blockId}' cannot be disabled.`, { blockId });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (definition.locked
|
|
173
|
+
&& !placements.some((placement) => placement.blockId === blockId && disabledBlocks.has(placement.blockId))) {
|
|
174
|
+
addDiagnostic(diagnostics, "locked-block-disabled", `Locked auto-flow block '${blockId}' cannot be disabled.`, { blockId });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return activePlacements;
|
|
178
|
+
}
|
|
179
|
+
function validateLockedPresetBlocks(preset, placements, disabledBlocks, diagnostics) {
|
|
180
|
+
for (const presetPlacement of preset.blocks) {
|
|
181
|
+
const definition = getBuiltInAutoFlowBlockDefinition(presetPlacement.blockId);
|
|
182
|
+
if (!definition?.locked && !presetPlacement.locked) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const matchingPlacement = placements.find((placement) => placement.blockId === presetPlacement.blockId);
|
|
186
|
+
if (!matchingPlacement) {
|
|
187
|
+
addDiagnostic(diagnostics, "locked-block-removed", `Locked auto-flow block '${presetPlacement.blockId}' cannot be removed.`, { blockId: presetPlacement.blockId, slotId: presetPlacement.slot });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!matchingPlacement.enabled || disabledBlocks.has(matchingPlacement.blockId)) {
|
|
191
|
+
addDiagnostic(diagnostics, "locked-block-disabled", `Locked auto-flow block '${presetPlacement.blockId}' cannot be disabled.`, { blockId: presetPlacement.blockId, slotId: matchingPlacement.slot });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function validatePlacements(placements, diagnostics) {
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const placement of placements) {
|
|
198
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
199
|
+
if (!definition) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (seen.has(placement.blockId)) {
|
|
203
|
+
addDiagnostic(diagnostics, "duplicate-block", `Auto-flow block '${placement.blockId}' is placed more than once.`, { blockId: placement.blockId, slotId: placement.slot });
|
|
204
|
+
}
|
|
205
|
+
seen.add(placement.blockId);
|
|
206
|
+
if (!definition.allowedSlots.includes(placement.slot)) {
|
|
207
|
+
addDiagnostic(diagnostics, "invalid-slot", `Auto-flow block '${placement.blockId}' cannot be placed in slot '${placement.slot}'. Allowed slots: ${definition.allowedSlots.join(", ")}.`, {
|
|
208
|
+
blockId: placement.blockId,
|
|
209
|
+
slotId: placement.slot,
|
|
210
|
+
allowedSlots: definition.allowedSlots,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function formatValue(value) {
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return `'${value}'`;
|
|
218
|
+
}
|
|
219
|
+
return JSON.stringify(value);
|
|
220
|
+
}
|
|
221
|
+
function fingerprint(value) {
|
|
222
|
+
return `sha256:${createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
223
|
+
}
|
|
224
|
+
function defaultConfigForPreset(preset) {
|
|
225
|
+
return {
|
|
226
|
+
kind: "auto-flow-config",
|
|
227
|
+
version: 1,
|
|
228
|
+
name: `preset-${preset}`,
|
|
229
|
+
basePreset: preset,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function sourceForPreset(preset) {
|
|
233
|
+
return { type: "preset", preset };
|
|
234
|
+
}
|
|
235
|
+
function sourceForLoadedConfig(loaded) {
|
|
236
|
+
return {
|
|
237
|
+
type: loaded.source.type === "project" ? "project-config" : "user-config",
|
|
238
|
+
configName: loaded.config.name,
|
|
239
|
+
path: loaded.source.path,
|
|
240
|
+
...(loaded.source.shadowedUserPath ? { shadowedUserPath: loaded.source.shadowedUserPath } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function resolveSlot(preset, config, slot) {
|
|
244
|
+
const defaults = DEFAULT_BLOCKS[preset][slot];
|
|
245
|
+
const override = config.slots?.[slot];
|
|
246
|
+
const configuredBlocks = override?.blocks;
|
|
247
|
+
const rawBlocks = configuredBlocks ?? defaults.map((id) => ({ id, enabled: "auto" }));
|
|
248
|
+
const decisions = [];
|
|
249
|
+
for (const block of rawBlocks) {
|
|
250
|
+
const registry = BLOCK_REGISTRY[block.id];
|
|
251
|
+
const enabled = block.enabled ?? "auto";
|
|
252
|
+
const included = enabled !== false;
|
|
253
|
+
const maxIterations = block.maxIterations ?? registry.defaultMaxIterations;
|
|
254
|
+
decisions.push({
|
|
255
|
+
slot,
|
|
256
|
+
blockId: block.id,
|
|
257
|
+
enabled,
|
|
258
|
+
included,
|
|
259
|
+
reason: included
|
|
260
|
+
? configuredBlocks
|
|
261
|
+
? "included by saved config"
|
|
262
|
+
: "included by preset default"
|
|
263
|
+
: "disabled by saved config",
|
|
264
|
+
...(maxIterations !== undefined ? { maxIterations } : {}),
|
|
265
|
+
...(registry.defaultMaxIterations !== undefined ? { defaultMaxIterations: registry.defaultMaxIterations } : {}),
|
|
266
|
+
...(registry.builtInFlowFileName ? { flowFileName: registry.builtInFlowFileName } : {}),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (configuredBlocks) {
|
|
270
|
+
const configuredIds = new Set(configuredBlocks.map((block) => block.id));
|
|
271
|
+
for (const defaultBlockId of defaults) {
|
|
272
|
+
if (configuredIds.has(defaultBlockId)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const registry = BLOCK_REGISTRY[defaultBlockId];
|
|
276
|
+
decisions.push({
|
|
277
|
+
slot,
|
|
278
|
+
blockId: defaultBlockId,
|
|
279
|
+
enabled: false,
|
|
280
|
+
included: false,
|
|
281
|
+
reason: "skipped because the slot override omitted this preset default block",
|
|
282
|
+
...(registry.defaultMaxIterations !== undefined ? { defaultMaxIterations: registry.defaultMaxIterations } : {}),
|
|
283
|
+
...(registry.defaultMaxIterations !== undefined ? { maxIterations: registry.defaultMaxIterations } : {}),
|
|
284
|
+
...(registry.builtInFlowFileName ? { flowFileName: registry.builtInFlowFileName } : {}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
decisions,
|
|
290
|
+
included: decisions.filter((decision) => decision.included),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function resolveAllSlots(config) {
|
|
294
|
+
const includedBySlot = {
|
|
295
|
+
designReview: [],
|
|
296
|
+
postImplementationChecks: [],
|
|
297
|
+
review: [],
|
|
298
|
+
final: [],
|
|
299
|
+
};
|
|
300
|
+
const decisions = [];
|
|
301
|
+
for (const slot of AUTO_FLOW_SLOT_NAMES) {
|
|
302
|
+
const resolution = resolveSlot(config.basePreset, config, slot);
|
|
303
|
+
decisions.push(...resolution.decisions);
|
|
304
|
+
includedBySlot[slot] = resolution.included;
|
|
305
|
+
}
|
|
306
|
+
return { decisions, includedBySlot };
|
|
307
|
+
}
|
|
308
|
+
function usesBuiltInShape(preset, includedBySlot) {
|
|
309
|
+
for (const slot of AUTO_FLOW_SLOT_NAMES) {
|
|
310
|
+
const defaults = DEFAULT_BLOCKS[preset][slot];
|
|
311
|
+
const included = includedBySlot[slot];
|
|
312
|
+
if (included.length !== defaults.length) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
for (let index = 0; index < defaults.length; index += 1) {
|
|
316
|
+
const decision = included[index];
|
|
317
|
+
if (!decision || decision.blockId !== defaults[index]) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const defaultMax = BLOCK_REGISTRY[decision.blockId].defaultMaxIterations;
|
|
321
|
+
if (decision.maxIterations !== undefined && defaultMax !== undefined && decision.maxIterations !== defaultMax) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
function builtInPhase(spec, id) {
|
|
329
|
+
const phase = spec.phases.find((item) => !("repeat" in item) && item.id === id);
|
|
330
|
+
if (!phase) {
|
|
331
|
+
throw new TaskRunnerError(`Built-in auto flow phase '${id}' was not found.`);
|
|
332
|
+
}
|
|
333
|
+
return cloneJson(phase);
|
|
334
|
+
}
|
|
335
|
+
function phaseLabel(id) {
|
|
336
|
+
switch (id) {
|
|
337
|
+
case "source":
|
|
338
|
+
return "source";
|
|
339
|
+
case "normalize":
|
|
340
|
+
return "normalize";
|
|
341
|
+
case "plan":
|
|
342
|
+
return "planning";
|
|
343
|
+
case "design_review_loop":
|
|
344
|
+
return "design review";
|
|
345
|
+
case "implement":
|
|
346
|
+
return "implementation";
|
|
347
|
+
case "review-loop":
|
|
348
|
+
return "review";
|
|
349
|
+
case "post_go_linter_loop":
|
|
350
|
+
return "post-implementation Go linter";
|
|
351
|
+
case "post_go_tests_loop":
|
|
352
|
+
return "post-implementation Go tests";
|
|
353
|
+
case "final_go_linter_loop":
|
|
354
|
+
return "final Go linter";
|
|
355
|
+
case "final_go_tests_loop":
|
|
356
|
+
return "final Go tests";
|
|
357
|
+
default:
|
|
358
|
+
return id;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function resolvedPhase(id, decision) {
|
|
362
|
+
return {
|
|
363
|
+
id,
|
|
364
|
+
label: phaseLabel(id),
|
|
365
|
+
...(decision ? { blockId: decision.blockId, slot: decision.slot } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function setFirstFlowRunFileName(phase, fileName) {
|
|
369
|
+
const next = cloneJson(phase);
|
|
370
|
+
const step = next.steps.find((candidate) => candidate.node === "flow-run");
|
|
371
|
+
if (!step) {
|
|
372
|
+
throw new TaskRunnerError(`Phase '${phase.id}' does not contain a flow-run step.`);
|
|
373
|
+
}
|
|
374
|
+
step.params = {
|
|
375
|
+
...(step.params ?? {}),
|
|
376
|
+
fileName: { const: fileName },
|
|
377
|
+
};
|
|
378
|
+
return next;
|
|
379
|
+
}
|
|
380
|
+
function valueRef(ref) {
|
|
381
|
+
return { ref };
|
|
382
|
+
}
|
|
383
|
+
function valueConst(value) {
|
|
384
|
+
return { const: value };
|
|
385
|
+
}
|
|
386
|
+
function checkPhaseId(slot, blockId) {
|
|
387
|
+
const prefix = slot === "final" ? "final" : "post";
|
|
388
|
+
if (blockId === "checks.go.linter") {
|
|
389
|
+
return `${prefix}_go_linter_loop`;
|
|
390
|
+
}
|
|
391
|
+
if (blockId === "checks.go.tests") {
|
|
392
|
+
return `${prefix}_go_tests_loop`;
|
|
393
|
+
}
|
|
394
|
+
throw new TaskRunnerError(`Block '${blockId}' cannot be rendered as a check phase.`);
|
|
395
|
+
}
|
|
396
|
+
function checkPhase(decision, fileName) {
|
|
397
|
+
const isLinter = decision.blockId === "checks.go.linter";
|
|
398
|
+
return {
|
|
399
|
+
id: checkPhaseId(decision.slot, decision.blockId),
|
|
400
|
+
steps: [
|
|
401
|
+
{
|
|
402
|
+
id: isLinter ? "run_go_linter_loop" : "run_go_tests_loop",
|
|
403
|
+
node: "flow-run",
|
|
404
|
+
params: {
|
|
405
|
+
fileName: valueConst(fileName),
|
|
406
|
+
labelText: valueConst(isLinter ? "Running Go linter loop" : "Running Go tests loop"),
|
|
407
|
+
taskKey: valueRef("params.taskKey"),
|
|
408
|
+
workspaceDir: valueRef("params.workspaceDir"),
|
|
409
|
+
extraPrompt: valueRef("params.extraPrompt"),
|
|
410
|
+
llmExecutor: valueRef("params.llmExecutor"),
|
|
411
|
+
llmModel: valueRef("params.llmModel"),
|
|
412
|
+
...(isLinter
|
|
413
|
+
? {
|
|
414
|
+
runGoLinterScript: valueRef("params.runGoLinterScript"),
|
|
415
|
+
runGoLinterIteration: valueRef("params.runGoLinterIteration"),
|
|
416
|
+
}
|
|
417
|
+
: {
|
|
418
|
+
runGoTestsScript: valueRef("params.runGoTestsScript"),
|
|
419
|
+
runGoTestsIteration: valueRef("params.runGoTestsIteration"),
|
|
420
|
+
}),
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function replaceOldMax(value, oldMax, newMax) {
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
return value.map((item) => replaceOldMax(item, oldMax, newMax));
|
|
429
|
+
}
|
|
430
|
+
if (value && typeof value === "object") {
|
|
431
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
432
|
+
key,
|
|
433
|
+
replaceOldMax(item, oldMax, newMax),
|
|
434
|
+
]));
|
|
435
|
+
}
|
|
436
|
+
if (typeof value === "number" && value === oldMax) {
|
|
437
|
+
return newMax;
|
|
438
|
+
}
|
|
439
|
+
if (typeof value === "string") {
|
|
440
|
+
return value
|
|
441
|
+
.replaceAll(String(oldMax), String(newMax))
|
|
442
|
+
.replaceAll(`_${oldMax}`, `_${newMax}`);
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
function isRepeatPhase(item) {
|
|
447
|
+
return "repeat" in item;
|
|
448
|
+
}
|
|
449
|
+
function withMaxIterations(spec, oldMax, newMax) {
|
|
450
|
+
if (newMax === oldMax) {
|
|
451
|
+
return cloneJson(spec);
|
|
452
|
+
}
|
|
453
|
+
const next = replaceOldMax(cloneJson(spec), oldMax, newMax);
|
|
454
|
+
const phases = [];
|
|
455
|
+
for (const item of next.phases) {
|
|
456
|
+
if (!isRepeatPhase(item)) {
|
|
457
|
+
phases.push(item);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (item.repeat.to !== newMax) {
|
|
461
|
+
phases.push(item);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (newMax < item.repeat.from) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
phases.push(item);
|
|
468
|
+
}
|
|
469
|
+
next.phases = phases;
|
|
470
|
+
return next;
|
|
471
|
+
}
|
|
472
|
+
function loadNamedBuiltInSpec(name, cwd) {
|
|
473
|
+
const ref = resolveNamedDeclarativeFlowRef(name, cwd);
|
|
474
|
+
return ref.source === "built-in"
|
|
475
|
+
? loadFlowSpecSync({ source: "built-in", fileName: ref.fileName })
|
|
476
|
+
: loadFlowSpecSync(ref);
|
|
477
|
+
}
|
|
478
|
+
function generatedNestedFileName(blockId, maxIterations) {
|
|
479
|
+
return `generated-${blockId.replaceAll(".", "-")}-${maxIterations}.json`;
|
|
480
|
+
}
|
|
481
|
+
async function resolveNestedFlowName(decision, cwd, inMemoryFlows, nestedFlowSpecs) {
|
|
482
|
+
const registry = BLOCK_REGISTRY[decision.blockId];
|
|
483
|
+
if (!registry.builtInFlowFileName || !registry.builtInSpecName || !registry.defaultMaxIterations) {
|
|
484
|
+
throw new TaskRunnerError(`Block '${decision.blockId}' does not have a runnable flow template.`);
|
|
485
|
+
}
|
|
486
|
+
const maxIterations = decision.maxIterations ?? registry.defaultMaxIterations;
|
|
487
|
+
if (maxIterations === registry.defaultMaxIterations) {
|
|
488
|
+
return registry.builtInFlowFileName;
|
|
489
|
+
}
|
|
490
|
+
const fileName = generatedNestedFileName(decision.blockId, maxIterations);
|
|
491
|
+
if (!inMemoryFlows[fileName]) {
|
|
492
|
+
const baseSpec = loadNamedBuiltInSpec(registry.builtInSpecName, cwd);
|
|
493
|
+
const generatedSpec = withMaxIterations(baseSpec, registry.defaultMaxIterations, maxIterations);
|
|
494
|
+
nestedFlowSpecs[fileName] = generatedSpec;
|
|
495
|
+
inMemoryFlows[fileName] = await loadDeclarativeFlowFromSpec(generatedSpec, { fileName }, {
|
|
496
|
+
cwd,
|
|
497
|
+
inMemoryFlows,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return fileName;
|
|
501
|
+
}
|
|
502
|
+
async function buildGeneratedFlow(config, includedBySlot, cwd) {
|
|
503
|
+
const baseSpec = loadBuiltInFlowSpecSync(builtInSpecFileForPreset(config.basePreset));
|
|
504
|
+
const commonSpec = loadBuiltInFlowSpecSync("auto-common.json");
|
|
505
|
+
const phases = [
|
|
506
|
+
builtInPhase(baseSpec, "source"),
|
|
507
|
+
builtInPhase(baseSpec, "normalize"),
|
|
508
|
+
builtInPhase(baseSpec, "plan"),
|
|
509
|
+
];
|
|
510
|
+
const resolvedPhases = [
|
|
511
|
+
resolvedPhase("source"),
|
|
512
|
+
resolvedPhase("normalize"),
|
|
513
|
+
resolvedPhase("plan"),
|
|
514
|
+
];
|
|
515
|
+
const inMemoryFlows = {};
|
|
516
|
+
const nestedFlowSpecs = {};
|
|
517
|
+
for (const decision of includedBySlot.designReview) {
|
|
518
|
+
if (decision.blockId !== "review.design-loop") {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const fileName = await resolveNestedFlowName(decision, cwd, inMemoryFlows, nestedFlowSpecs);
|
|
522
|
+
phases.push(setFirstFlowRunFileName(builtInPhase(commonSpec, "design_review_loop"), fileName));
|
|
523
|
+
resolvedPhases.push(resolvedPhase("design_review_loop", decision));
|
|
524
|
+
}
|
|
525
|
+
phases.push(builtInPhase(baseSpec, "implement"));
|
|
526
|
+
resolvedPhases.push(resolvedPhase("implement"));
|
|
527
|
+
for (const decision of includedBySlot.postImplementationChecks) {
|
|
528
|
+
if (decision.blockId !== "checks.go.linter" && decision.blockId !== "checks.go.tests") {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const fileName = await resolveNestedFlowName(decision, cwd, inMemoryFlows, nestedFlowSpecs);
|
|
532
|
+
phases.push(checkPhase(decision, fileName));
|
|
533
|
+
resolvedPhases.push(resolvedPhase(checkPhaseId(decision.slot, decision.blockId), decision));
|
|
534
|
+
}
|
|
535
|
+
for (const decision of includedBySlot.review) {
|
|
536
|
+
if (decision.blockId !== "review.loop") {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const fileName = await resolveNestedFlowName(decision, cwd, inMemoryFlows, nestedFlowSpecs);
|
|
540
|
+
phases.push(setFirstFlowRunFileName(builtInPhase(baseSpec, "review-loop"), fileName));
|
|
541
|
+
resolvedPhases.push(resolvedPhase("review-loop", decision));
|
|
542
|
+
}
|
|
543
|
+
for (const decision of includedBySlot.final) {
|
|
544
|
+
if (decision.blockId !== "checks.go.linter" && decision.blockId !== "checks.go.tests") {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const fileName = await resolveNestedFlowName(decision, cwd, inMemoryFlows, nestedFlowSpecs);
|
|
548
|
+
phases.push(checkPhase(decision, fileName));
|
|
549
|
+
resolvedPhases.push(resolvedPhase(checkPhaseId(decision.slot, decision.blockId), decision));
|
|
550
|
+
}
|
|
551
|
+
const flowSpec = {
|
|
552
|
+
kind: "auto-flow",
|
|
553
|
+
version: 1,
|
|
554
|
+
description: `Generated configurable auto flow from the ${config.basePreset} preset.`,
|
|
555
|
+
phases,
|
|
556
|
+
};
|
|
557
|
+
const flow = await loadDeclarativeFlowFromSpec(flowSpec, { fileName: "resolved-auto-flow.json" }, {
|
|
558
|
+
cwd,
|
|
559
|
+
inMemoryFlows,
|
|
560
|
+
});
|
|
561
|
+
return { flow, inMemoryFlows, flowSpec, nestedFlowSpecs, phases: resolvedPhases };
|
|
562
|
+
}
|
|
563
|
+
function artifactPolicy(scopeKey) {
|
|
564
|
+
return {
|
|
565
|
+
dryRunFlowWritesArtifacts: false,
|
|
566
|
+
nonDryRunWritesArtifacts: true,
|
|
567
|
+
...(scopeKey
|
|
568
|
+
? {
|
|
569
|
+
artifactPaths: {
|
|
570
|
+
flowConfigYaml: flowConfigYamlFile(scopeKey),
|
|
571
|
+
resolvedFlowJson: resolvedFlowJsonFile(scopeKey),
|
|
572
|
+
resolvedFlowSummaryJson: resolvedFlowSummaryJsonFile(scopeKey),
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
: {}),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function buildSummary(document) {
|
|
579
|
+
return {
|
|
580
|
+
schemaVersion: 1,
|
|
581
|
+
source: document.source,
|
|
582
|
+
basePreset: document.basePreset,
|
|
583
|
+
selectedCommand: document.selectedCommand,
|
|
584
|
+
phaseOrder: document.phases.map((phase) => phase.label),
|
|
585
|
+
includedBlocks: document.blockDecisions.filter((decision) => decision.included),
|
|
586
|
+
skippedBlocks: document.blockDecisions.filter((decision) => !decision.included),
|
|
587
|
+
executionTarget: document.executionTarget.kind,
|
|
588
|
+
artifactPolicy: document.artifactPolicy,
|
|
589
|
+
fingerprint: document.fingerprint,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
export async function resolveAutoFlow(selection, options = {}) {
|
|
593
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
594
|
+
const loadedConfig = selection.kind === "config" ? loadAutoFlowConfigByName(selection.name, cwd) : null;
|
|
595
|
+
const config = loadedConfig?.config ?? defaultConfigForPreset(selection.kind === "preset" ? selection.preset : "standard");
|
|
596
|
+
const normalizedConfigYaml = loadedConfig?.normalizedYaml ?? normalizeAutoFlowConfigYaml(config);
|
|
597
|
+
const source = loadedConfig ? sourceForLoadedConfig(loadedConfig) : sourceForPreset(config.basePreset);
|
|
598
|
+
const selectedCommand = selectedCommandForPreset(config.basePreset);
|
|
599
|
+
const { decisions, includedBySlot } = resolveAllSlots(config);
|
|
600
|
+
const policy = artifactPolicy(options.scopeKey);
|
|
601
|
+
let execution;
|
|
602
|
+
let phases;
|
|
603
|
+
let executionTarget;
|
|
604
|
+
if (usesBuiltInShape(config.basePreset, includedBySlot)) {
|
|
605
|
+
const specFile = builtInSpecFileForPreset(config.basePreset);
|
|
606
|
+
phases = config.basePreset === "standard"
|
|
607
|
+
? [
|
|
608
|
+
resolvedPhase("source"),
|
|
609
|
+
resolvedPhase("normalize"),
|
|
610
|
+
resolvedPhase("plan"),
|
|
611
|
+
resolvedPhase("design_review_loop", includedBySlot.designReview[0]),
|
|
612
|
+
resolvedPhase("implement"),
|
|
613
|
+
resolvedPhase("review-loop", includedBySlot.review[0]),
|
|
614
|
+
]
|
|
615
|
+
: [
|
|
616
|
+
resolvedPhase("source"),
|
|
617
|
+
resolvedPhase("normalize"),
|
|
618
|
+
resolvedPhase("plan"),
|
|
619
|
+
resolvedPhase("implement"),
|
|
620
|
+
resolvedPhase("review-loop", includedBySlot.review[0]),
|
|
621
|
+
];
|
|
622
|
+
execution = { kind: "built-in", specFile };
|
|
623
|
+
executionTarget = { kind: "built-in", specFile };
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
const generated = await buildGeneratedFlow(config, includedBySlot, cwd);
|
|
627
|
+
phases = generated.phases;
|
|
628
|
+
execution = {
|
|
629
|
+
kind: "generated",
|
|
630
|
+
flow: generated.flow,
|
|
631
|
+
inMemoryFlows: generated.inMemoryFlows,
|
|
632
|
+
};
|
|
633
|
+
executionTarget = {
|
|
634
|
+
kind: "generated",
|
|
635
|
+
fileName: generated.flow.fileName,
|
|
636
|
+
nestedFlowFiles: Object.keys(generated.inMemoryFlows).sort((left, right) => left.localeCompare(right)),
|
|
637
|
+
flowSpec: generated.flowSpec,
|
|
638
|
+
nestedFlowSpecs: Object.fromEntries(Object.entries(generated.nestedFlowSpecs).sort(([left], [right]) => left.localeCompare(right))),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const documentWithoutFingerprint = {
|
|
642
|
+
schemaVersion: 1,
|
|
643
|
+
source,
|
|
644
|
+
requested: selection,
|
|
645
|
+
basePreset: config.basePreset,
|
|
646
|
+
selectedCommand,
|
|
647
|
+
phases,
|
|
648
|
+
blockDecisions: decisions,
|
|
649
|
+
artifactPolicy: policy,
|
|
650
|
+
executionTarget,
|
|
651
|
+
validationDiagnostics: [],
|
|
652
|
+
};
|
|
653
|
+
const document = {
|
|
654
|
+
...documentWithoutFingerprint,
|
|
655
|
+
fingerprint: fingerprint(documentWithoutFingerprint),
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
config,
|
|
659
|
+
normalizedConfigYaml,
|
|
660
|
+
document,
|
|
661
|
+
summary: buildSummary(document),
|
|
662
|
+
execution,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
export function formatAutoFlowDryRunPreview(resolved) {
|
|
666
|
+
const lines = [
|
|
667
|
+
"Auto flow dry-run preview",
|
|
668
|
+
`Source: ${resolved.document.source.type}`,
|
|
669
|
+
];
|
|
670
|
+
if (resolved.document.source.type === "preset") {
|
|
671
|
+
lines.push(`Preset: ${resolved.document.source.preset}`);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
lines.push(`Config: ${resolved.document.source.configName}`);
|
|
675
|
+
lines.push(`Config path: ${resolved.document.source.path}`);
|
|
676
|
+
if (resolved.document.source.shadowedUserPath) {
|
|
677
|
+
lines.push(`Precedence: project config selected; user config shadowed at ${resolved.document.source.shadowedUserPath}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
lines.push(`Base preset: ${resolved.document.basePreset}`);
|
|
681
|
+
lines.push(`Execution target: ${resolved.document.executionTarget.kind}`);
|
|
682
|
+
lines.push("", "Phases:");
|
|
683
|
+
resolved.document.phases.forEach((phase, index) => {
|
|
684
|
+
lines.push(` ${index + 1}. ${phase.label} (${phase.id})`);
|
|
685
|
+
});
|
|
686
|
+
lines.push("", "Blocks:");
|
|
687
|
+
for (const decision of resolved.document.blockDecisions) {
|
|
688
|
+
const state = decision.included ? "included" : "skipped";
|
|
689
|
+
const iterationText = decision.maxIterations ? `, maxIterations=${decision.maxIterations}` : "";
|
|
690
|
+
lines.push(` - ${decision.slot}/${decision.blockId}: ${state} (${decision.reason}${iterationText})`);
|
|
691
|
+
}
|
|
692
|
+
lines.push("", "Artifact policy:", " - --dry-run-flow writes no resolver artifacts.", " - Non-dry configurable auto runs write flow-config.yaml, resolved-flow.json, and resolved-flow-summary.json.", "", "No workflow steps were run.");
|
|
693
|
+
return `${lines.join("\n")}\n`;
|
|
694
|
+
}
|
|
695
|
+
function writeTextAtomic(filePath, content) {
|
|
696
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
697
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
698
|
+
writeFileSync(tempPath, content, "utf8");
|
|
699
|
+
renameSync(tempPath, filePath);
|
|
700
|
+
}
|
|
701
|
+
export function persistResolvedAutoFlowArtifacts(scopeKey, resolved) {
|
|
702
|
+
writeTextAtomic(flowConfigYamlFile(scopeKey), resolved.normalizedConfigYaml);
|
|
703
|
+
writeTextAtomic(resolvedFlowJsonFile(scopeKey), `${JSON.stringify(resolved.document, null, 2)}\n`);
|
|
704
|
+
writeTextAtomic(resolvedFlowSummaryJsonFile(scopeKey), `${JSON.stringify(resolved.summary, null, 2)}\n`);
|
|
705
|
+
}
|
|
706
|
+
function validateParameters(placements, diagnostics) {
|
|
707
|
+
for (const placement of placements) {
|
|
708
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
709
|
+
if (!definition) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const paramDefinitions = definition.params ?? {};
|
|
713
|
+
for (const paramName of Object.keys(placement.params)) {
|
|
714
|
+
if (!paramDefinitions[paramName]) {
|
|
715
|
+
addDiagnostic(diagnostics, "unknown-parameter", `Auto-flow block '${placement.blockId}' does not support parameter '${paramName}'.`, { blockId: placement.blockId, slotId: placement.slot, paramName });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const [paramName, paramDefinition] of Object.entries(paramDefinitions)) {
|
|
719
|
+
const value = Object.prototype.hasOwnProperty.call(placement.params, paramName)
|
|
720
|
+
? placement.params[paramName]
|
|
721
|
+
: paramDefinition.default;
|
|
722
|
+
if (!Number.isInteger(value)) {
|
|
723
|
+
addDiagnostic(diagnostics, "invalid-parameter-type", `Auto-flow block '${placement.blockId}' parameter '${paramName}' must be an integer; received ${formatValue(value)}.`, { blockId: placement.blockId, slotId: placement.slot, paramName, value });
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const numericValue = value;
|
|
727
|
+
if (numericValue < paramDefinition.min || numericValue > paramDefinition.max) {
|
|
728
|
+
addDiagnostic(diagnostics, "parameter-out-of-range", `Auto-flow block '${placement.blockId}' parameter '${paramName}' must be between ${paramDefinition.min} and ${paramDefinition.max}; received ${numericValue}.`, { blockId: placement.blockId, slotId: placement.slot, paramName, value: numericValue });
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!paramDefinition.supportedExecutableValues.includes(numericValue)) {
|
|
732
|
+
addDiagnostic(diagnostics, "unsupported-override", `Auto-flow block '${placement.blockId}' parameter '${paramName}' value ${numericValue} is not executable in this implementation; supported executable values: ${paramDefinition.supportedExecutableValues.join(", ")}.`, { blockId: placement.blockId, slotId: placement.slot, paramName, value: numericValue });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function validateOverrideBlockParams(override, diagnostics) {
|
|
738
|
+
if (!override?.blockParams) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
for (const blockId of Object.keys(override.blockParams)) {
|
|
742
|
+
if (!getBuiltInAutoFlowBlockDefinition(blockId)) {
|
|
743
|
+
addDiagnostic(diagnostics, "unknown-block", `Unknown auto-flow block '${blockId}' has parameter overrides.`, { blockId });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function validateDependencies(placements, diagnostics) {
|
|
748
|
+
const provided = new Set();
|
|
749
|
+
for (const placement of sortedPlacements(placements)) {
|
|
750
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
751
|
+
if (!definition) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const missing = definition.requires.filter((contract) => !provided.has(contract));
|
|
755
|
+
for (const contract of missing) {
|
|
756
|
+
addDiagnostic(diagnostics, "missing-dependency", `Auto-flow block '${placement.blockId}' requires contract '${contract}' before slot '${placement.slot}'.`, {
|
|
757
|
+
blockId: placement.blockId,
|
|
758
|
+
slotId: placement.slot,
|
|
759
|
+
missingContract: contract,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (missing.length === 0) {
|
|
763
|
+
for (const contract of definition.provides) {
|
|
764
|
+
provided.add(contract);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function buildSpec(preset, placements) {
|
|
770
|
+
return {
|
|
771
|
+
kind: preset.kind,
|
|
772
|
+
version: preset.version,
|
|
773
|
+
description: preset.description,
|
|
774
|
+
phases: sortedPlacements(placements).map((placement) => {
|
|
775
|
+
const definition = getBuiltInAutoFlowBlockDefinition(placement.blockId);
|
|
776
|
+
if (!definition || !isAutoFlowSlotId(placement.slot)) {
|
|
777
|
+
throw new TaskRunnerError(`Cannot generate phase for invalid auto-flow block '${placement.blockId}'.`);
|
|
778
|
+
}
|
|
779
|
+
return definition.createPhase({
|
|
780
|
+
presetId: preset.id,
|
|
781
|
+
blockId: placement.blockId,
|
|
782
|
+
slotId: placement.slot,
|
|
783
|
+
params: placement.params,
|
|
784
|
+
});
|
|
785
|
+
}),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
export function resolveAutoFlowPreset(presetOrId, override) {
|
|
789
|
+
const preset = typeof presetOrId === "string" ? getBuiltInAutoFlowPreset(presetOrId) : presetOrId;
|
|
790
|
+
const summary = createSummary(preset.id);
|
|
791
|
+
const diagnostics = [];
|
|
792
|
+
const disabledBlocks = new Set(override?.disabledBlocks ?? []);
|
|
793
|
+
const placements = workingPlacementsForPreset(preset, override);
|
|
794
|
+
validateOverrideBlockParams(override, diagnostics);
|
|
795
|
+
validateLockedPresetBlocks(preset, placements, disabledBlocks, diagnostics);
|
|
796
|
+
const activePlacements = validateDisabledBlocks(preset, placements, disabledBlocks, summary, diagnostics);
|
|
797
|
+
validatePlacements(activePlacements, diagnostics);
|
|
798
|
+
validateParameters(activePlacements, diagnostics);
|
|
799
|
+
validateDependencies(activePlacements, diagnostics);
|
|
800
|
+
summary.invalid.push(...diagnostics.map(decisionFromDiagnostic));
|
|
801
|
+
if (diagnostics.length > 0) {
|
|
802
|
+
return { preset, summary, diagnostics };
|
|
803
|
+
}
|
|
804
|
+
for (const placement of sortedPlacements(activePlacements)) {
|
|
805
|
+
summary.enabled.push({
|
|
806
|
+
status: "enabled",
|
|
807
|
+
blockId: placement.blockId,
|
|
808
|
+
slotId: placement.slot,
|
|
809
|
+
reason: `Block '${placement.blockId}' is enabled.`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
preset,
|
|
814
|
+
summary,
|
|
815
|
+
diagnostics,
|
|
816
|
+
spec: buildSpec(preset, activePlacements),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
export function resolveBuiltInAutoFlowSpecByFileName(fileName) {
|
|
820
|
+
const preset = getBuiltInAutoFlowPresetByFileName(fileName);
|
|
821
|
+
if (!preset) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const result = resolveAutoFlowPreset(preset);
|
|
825
|
+
if (!result.spec) {
|
|
826
|
+
const details = result.diagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
827
|
+
throw new TaskRunnerError(`Failed to resolve built-in auto-flow '${fileName}'.\n${details}`);
|
|
828
|
+
}
|
|
829
|
+
return result.spec;
|
|
830
|
+
}
|