agentweaver 0.1.15 → 0.1.17
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 +76 -19
- package/dist/artifact-manifest.js +219 -0
- package/dist/artifacts.js +88 -3
- package/dist/doctor/checks/env-diagnostics.js +25 -0
- package/dist/doctor/checks/executors.js +2 -2
- package/dist/doctor/checks/flow-readiness.js +15 -18
- package/dist/flow-state.js +212 -15
- package/dist/index.js +539 -209
- package/dist/interactive/blessed-session.js +361 -0
- package/dist/interactive/controller.js +1326 -0
- package/dist/interactive/create-interactive-session.js +5 -0
- package/dist/interactive/ink/index.js +597 -0
- package/dist/interactive/progress.js +245 -0
- package/dist/interactive/selectors.js +14 -0
- package/dist/interactive/session.js +1 -0
- package/dist/interactive/state.js +34 -0
- package/dist/interactive/tree.js +155 -0
- package/dist/interactive/types.js +1 -0
- package/dist/interactive/view-model.js +1 -0
- package/dist/interactive-ui.js +159 -194
- package/dist/pipeline/auto-flow.js +9 -6
- package/dist/pipeline/context.js +7 -5
- package/dist/pipeline/declarative-flow-runner.js +212 -6
- package/dist/pipeline/declarative-flows.js +63 -17
- package/dist/pipeline/execution-routing-config.js +15 -0
- package/dist/pipeline/flow-catalog.js +50 -12
- package/dist/pipeline/flow-run-resume.js +29 -0
- package/dist/pipeline/flow-specs/auto-common.json +90 -360
- package/dist/pipeline/flow-specs/auto-golang.json +81 -360
- package/dist/pipeline/flow-specs/auto-simple.json +141 -0
- package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
- package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +316 -0
- package/dist/pipeline/flow-specs/design-review.json +10 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
- package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
- package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
- package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +13 -6
- package/dist/pipeline/flow-specs/instant-task.json +177 -0
- package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
- package/dist/pipeline/flow-specs/plan-revise.json +7 -1
- package/dist/pipeline/flow-specs/plan.json +51 -71
- package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
- package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
- package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
- package/dist/pipeline/flow-specs/review/review-project.json +12 -0
- package/dist/pipeline/flow-specs/review/review.json +37 -31
- package/dist/pipeline/flow-specs/task-describe.json +2 -0
- package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
- package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
- package/dist/pipeline/launch-profile-config.js +30 -18
- package/dist/pipeline/node-contract.js +1 -0
- package/dist/pipeline/node-registry.js +115 -6
- package/dist/pipeline/node-runner.js +3 -2
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
- package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
- package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
- package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
- package/dist/pipeline/nodes/flow-run-node.js +242 -8
- package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
- package/dist/pipeline/nodes/llm-prompt-node.js +38 -36
- package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
- package/dist/pipeline/nodes/review-verdict-node.js +86 -0
- package/dist/pipeline/nodes/select-files-form-node.js +8 -0
- package/dist/pipeline/nodes/structured-summary-node.js +24 -0
- package/dist/pipeline/nodes/user-input-node.js +38 -3
- package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
- package/dist/pipeline/plugin-loader.js +389 -0
- package/dist/pipeline/plugin-types.js +1 -0
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/prompt-runtime.js +4 -1
- package/dist/pipeline/registry.js +71 -4
- package/dist/pipeline/review-iteration.js +26 -0
- package/dist/pipeline/spec-compiler.js +3 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +3 -0
- package/dist/pipeline/spec-validator.js +20 -0
- package/dist/pipeline/value-resolver.js +76 -2
- package/dist/plugin-sdk.js +1 -0
- package/dist/prompts.js +36 -14
- package/dist/review-severity.js +45 -0
- package/dist/runtime/artifact-registry.js +405 -0
- package/dist/runtime/design-review-input-contract.js +17 -16
- package/dist/runtime/env-loader.js +3 -0
- package/dist/runtime/execution-routing-store.js +134 -0
- package/dist/runtime/execution-routing.js +233 -0
- package/dist/runtime/interactive-execution-routing.js +471 -0
- package/dist/runtime/plan-revise-input-contract.js +35 -32
- package/dist/runtime/planning-bundle.js +123 -0
- package/dist/runtime/ready-to-merge.js +22 -1
- package/dist/runtime/review-input-contract.js +100 -0
- package/dist/structured-artifact-schema-registry.js +9 -0
- package/dist/structured-artifact-schemas.json +140 -1
- package/dist/structured-artifacts.js +77 -6
- package/dist/user-input.js +70 -3
- package/docs/example/.flows/examples/claude-example.json +50 -0
- package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/examples/.flows/claude-example.json +50 -0
- package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
- package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
- package/docs/plugin-sdk.md +731 -0
- package/package.json +11 -4
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { flowRoutingGroups, flowRoutingKey } from "../pipeline/flow-catalog.js";
|
|
3
|
+
import { loadNamedDeclarativeFlow } from "../pipeline/declarative-flows.js";
|
|
4
|
+
import { createPipelineRegistryContext } from "../pipeline/plugin-loader.js";
|
|
5
|
+
import { EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
|
|
6
|
+
import { DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, } from "../pipeline/launch-profile-config.js";
|
|
7
|
+
import { FlowInterruptedError, TaskRunnerError } from "../errors.js";
|
|
8
|
+
import { BUILT_IN_EXECUTION_PRESETS, builtInExecutionPresetList, modelOptionsForExecutor, resolveExecutionRouting, resolveStoredExecutionRoutingSnapshot, routingGroupLabel, } from "./execution-routing.js";
|
|
9
|
+
import { getFlowDefaultExecutionRouting, getLastUsedExecutionRouting, getNamedExecutionPresets, saveFlowDefaultExecutionRouting, saveLastUsedExecutionRouting, saveNamedExecutionPreset, } from "./execution-routing-store.js";
|
|
10
|
+
function executionPresetSelectionForm(flowEntry, flowDefaultAvailable, lastUsedAvailable, namedPresetNames) {
|
|
11
|
+
const options = [];
|
|
12
|
+
if (flowDefaultAvailable) {
|
|
13
|
+
options.push({
|
|
14
|
+
value: "flow-default",
|
|
15
|
+
label: "Flow default",
|
|
16
|
+
description: "Use the saved routing snapshot for this flow.",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (lastUsedAvailable) {
|
|
20
|
+
options.push({
|
|
21
|
+
value: "last-used",
|
|
22
|
+
label: "Last used",
|
|
23
|
+
description: "Reuse the most recent routing started for this flow.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
for (const preset of builtInExecutionPresetList()) {
|
|
27
|
+
options.push({
|
|
28
|
+
value: `built-in:${preset.id}`,
|
|
29
|
+
label: preset.label,
|
|
30
|
+
description: preset.description,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const name of namedPresetNames) {
|
|
34
|
+
options.push({
|
|
35
|
+
value: `named:${name}`,
|
|
36
|
+
label: `Named preset: ${name}`,
|
|
37
|
+
description: "Reuse a saved routing snapshot across flows.",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
options.push({
|
|
41
|
+
value: "custom",
|
|
42
|
+
label: "Custom",
|
|
43
|
+
description: "Start from the default route and edit the fallback route plus routing groups manually.",
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
formId: "flow-execution-preset",
|
|
47
|
+
title: "Execution Routing",
|
|
48
|
+
description: `Select an execution strategy for '${flowEntry.id}'.`,
|
|
49
|
+
submitLabel: "Continue",
|
|
50
|
+
fields: [
|
|
51
|
+
{
|
|
52
|
+
id: "preset",
|
|
53
|
+
type: "single-select",
|
|
54
|
+
label: "Preset",
|
|
55
|
+
required: true,
|
|
56
|
+
default: options[0]?.value ?? "custom",
|
|
57
|
+
options,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function selectedExecutorFromValues(values, fieldId, fallbackExecutor, registryContext) {
|
|
63
|
+
const candidate = typeof values[fieldId] === "string" ? values[fieldId] : "";
|
|
64
|
+
return registryContext.executors.getRouting(candidate)?.kind === "llm" ? candidate : fallbackExecutor;
|
|
65
|
+
}
|
|
66
|
+
function routingEditorDraftFromRouting(routing) {
|
|
67
|
+
return {
|
|
68
|
+
defaultRoute: {
|
|
69
|
+
executor: routing.defaultRoute.executor,
|
|
70
|
+
model: routing.defaultRoute.model,
|
|
71
|
+
},
|
|
72
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [
|
|
73
|
+
group,
|
|
74
|
+
{
|
|
75
|
+
executor: routing.groups[group].executor,
|
|
76
|
+
model: routing.groups[group].model,
|
|
77
|
+
},
|
|
78
|
+
])),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function advancedRoutingEditorForm(activeGroups, draft, registryContext, validationMessage) {
|
|
82
|
+
const executorOptions = registryContext.executors.llmExecutors().map((entry) => ({
|
|
83
|
+
value: entry.id,
|
|
84
|
+
label: entry.id,
|
|
85
|
+
}));
|
|
86
|
+
const fields = [
|
|
87
|
+
{
|
|
88
|
+
id: "default_route_executor",
|
|
89
|
+
type: "single-select",
|
|
90
|
+
label: "Default route executor",
|
|
91
|
+
required: true,
|
|
92
|
+
default: draft.defaultRoute.executor,
|
|
93
|
+
options: executorOptions,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "default_route_model",
|
|
97
|
+
type: "single-select",
|
|
98
|
+
label: "Default route model",
|
|
99
|
+
help: "Available models follow the selected default executor.",
|
|
100
|
+
required: true,
|
|
101
|
+
default: draft.defaultRoute.model,
|
|
102
|
+
options: modelOptionsForExecutor(draft.defaultRoute.executor, registryContext.executors),
|
|
103
|
+
optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, "default_route_executor", draft.defaultRoute.executor, registryContext), registryContext.executors),
|
|
104
|
+
},
|
|
105
|
+
...activeGroups.flatMap((group) => {
|
|
106
|
+
const route = draft.groups[group];
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
id: `${group}_executor`,
|
|
110
|
+
type: "single-select",
|
|
111
|
+
label: `${routingGroupLabel(group)} executor`,
|
|
112
|
+
required: true,
|
|
113
|
+
default: route.executor,
|
|
114
|
+
options: executorOptions,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: `${group}_model`,
|
|
118
|
+
type: "single-select",
|
|
119
|
+
label: `${routingGroupLabel(group)} model`,
|
|
120
|
+
help: `Available models follow the selected ${routingGroupLabel(group)} executor.`,
|
|
121
|
+
required: true,
|
|
122
|
+
default: route.model,
|
|
123
|
+
options: modelOptionsForExecutor(route.executor, registryContext.executors),
|
|
124
|
+
optionsFromValues: (values) => modelOptionsForExecutor(selectedExecutorFromValues(values, `${group}_executor`, route.executor, registryContext), registryContext.executors),
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
return {
|
|
130
|
+
formId: "flow-routing-editor",
|
|
131
|
+
title: "Advanced Routing",
|
|
132
|
+
description: validationMessage
|
|
133
|
+
? `${activeGroups.length > 0
|
|
134
|
+
? "Edit the fallback route plus executor and model by routing group."
|
|
135
|
+
: "Edit the fallback executor and model used by this flow."}\n\n${validationMessage}`
|
|
136
|
+
: activeGroups.length > 0
|
|
137
|
+
? "Edit the fallback route plus executor and model by routing group."
|
|
138
|
+
: "Edit the fallback executor and model used by this flow.",
|
|
139
|
+
submitLabel: "Apply",
|
|
140
|
+
fields,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function routingActionForm(previewText) {
|
|
144
|
+
return {
|
|
145
|
+
formId: "flow-routing-action",
|
|
146
|
+
title: "Routing Preview",
|
|
147
|
+
description: "Review the effective routing and choose the next action.",
|
|
148
|
+
preview: previewText,
|
|
149
|
+
submitLabel: "Continue",
|
|
150
|
+
fields: [
|
|
151
|
+
{
|
|
152
|
+
id: "action",
|
|
153
|
+
type: "single-select",
|
|
154
|
+
label: "Action",
|
|
155
|
+
required: true,
|
|
156
|
+
default: "start",
|
|
157
|
+
options: [
|
|
158
|
+
{ value: "start", label: "Start", description: "Run the flow with the previewed routing." },
|
|
159
|
+
{ value: "edit", label: "Edit routing", description: "Open the advanced routing editor." },
|
|
160
|
+
{ value: "cancel", label: "Cancel", description: "Abort launching this flow." },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function routingPersistenceForm() {
|
|
167
|
+
return {
|
|
168
|
+
formId: "flow-routing-persistence",
|
|
169
|
+
title: "Save Routing",
|
|
170
|
+
description: "Choose how to persist the edited routing.",
|
|
171
|
+
submitLabel: "Continue",
|
|
172
|
+
fields: [
|
|
173
|
+
{
|
|
174
|
+
id: "persistence",
|
|
175
|
+
type: "single-select",
|
|
176
|
+
label: "Save mode",
|
|
177
|
+
required: true,
|
|
178
|
+
default: "current-run",
|
|
179
|
+
options: [
|
|
180
|
+
{ value: "current-run", label: "Current run only", description: "Use the edited routing only for this start." },
|
|
181
|
+
{ value: "flow-default", label: "Flow default", description: "Reuse this routing automatically for this flow." },
|
|
182
|
+
{ value: "named-preset", label: "Named preset", description: "Save this routing as a reusable preset." },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function presetNameForm() {
|
|
189
|
+
return {
|
|
190
|
+
formId: "flow-routing-preset-name",
|
|
191
|
+
title: "Preset Name",
|
|
192
|
+
description: "Enter a reusable name for the routing preset.",
|
|
193
|
+
submitLabel: "Save",
|
|
194
|
+
fields: [
|
|
195
|
+
{
|
|
196
|
+
id: "name",
|
|
197
|
+
type: "text",
|
|
198
|
+
label: "Preset name",
|
|
199
|
+
required: true,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function normalizeEditableRoute(label, route, executors) {
|
|
205
|
+
if (isAllowedModelForExecutor(route.executor, route.model, executors)) {
|
|
206
|
+
return {
|
|
207
|
+
route: { ...route },
|
|
208
|
+
validationErrors: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
route: {
|
|
213
|
+
executor: route.executor,
|
|
214
|
+
model: defaultModelForExecutor(route.executor, executors),
|
|
215
|
+
},
|
|
216
|
+
validationErrors: [
|
|
217
|
+
`${label} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`,
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function normalizeEditableRoutingDraft(draft, executors) {
|
|
222
|
+
const defaultRoute = normalizeEditableRoute("Default route", draft.defaultRoute, executors);
|
|
223
|
+
const groups = {};
|
|
224
|
+
const currentRunOverrides = {};
|
|
225
|
+
const validationErrors = [...defaultRoute.validationErrors];
|
|
226
|
+
for (const group of EXECUTION_ROUTING_GROUPS) {
|
|
227
|
+
const normalizedRoute = normalizeEditableRoute(routingGroupLabel(group), draft.groups[group], executors);
|
|
228
|
+
groups[group] = normalizedRoute.route;
|
|
229
|
+
if (normalizedRoute.route.executor !== defaultRoute.route.executor
|
|
230
|
+
|| normalizedRoute.route.model !== defaultRoute.route.model) {
|
|
231
|
+
currentRunOverrides[group] = normalizedRoute.route;
|
|
232
|
+
}
|
|
233
|
+
validationErrors.push(...normalizedRoute.validationErrors);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
draft: {
|
|
237
|
+
defaultRoute: defaultRoute.route,
|
|
238
|
+
groups,
|
|
239
|
+
},
|
|
240
|
+
currentRunOverrides,
|
|
241
|
+
validationErrors,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function normalizedEffectiveStepSignature(row) {
|
|
245
|
+
const normalizedStep = row.step.replace(/review_iteration_\d+/g, "review_iteration_*");
|
|
246
|
+
return [normalizedStep, row.group, row.executor, row.model].join("\u0000");
|
|
247
|
+
}
|
|
248
|
+
function collapseRepeatedEffectiveRoutedStepRows(rows) {
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
const collapsed = [];
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
const signature = normalizedEffectiveStepSignature(row);
|
|
253
|
+
if (seen.has(signature)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
seen.add(signature);
|
|
257
|
+
collapsed.push(row);
|
|
258
|
+
}
|
|
259
|
+
return collapsed;
|
|
260
|
+
}
|
|
261
|
+
function truncateTableCell(value, maxWidth) {
|
|
262
|
+
if (value.length <= maxWidth) {
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
if (maxWidth <= 1) {
|
|
266
|
+
return value.slice(0, maxWidth);
|
|
267
|
+
}
|
|
268
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
269
|
+
}
|
|
270
|
+
function formatAsciiTable(headers, rows, maxWidths) {
|
|
271
|
+
const widths = headers.map((header, index) => Math.min(maxWidths[index] ?? Number.MAX_SAFE_INTEGER, Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))));
|
|
272
|
+
const formatRow = (row) => `| ${row.map((cell, index) => truncateTableCell(cell ?? "", widths[index] ?? 1).padEnd(widths[index] ?? 1)).join(" | ")} |`;
|
|
273
|
+
const separator = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
|
|
274
|
+
return [formatRow(headers), separator, ...rows.map((row) => formatRow(row))];
|
|
275
|
+
}
|
|
276
|
+
function collectEffectiveRoutedStepRows(flow, cwd, routing, registryContext, prefixSegments = [], ancestry = []) {
|
|
277
|
+
if (ancestry.includes(flow.absolutePath)) {
|
|
278
|
+
return Promise.resolve([]);
|
|
279
|
+
}
|
|
280
|
+
const nextAncestry = [...ancestry, flow.absolutePath];
|
|
281
|
+
const rows = [];
|
|
282
|
+
const run = async () => {
|
|
283
|
+
for (const phase of flow.phases) {
|
|
284
|
+
for (const step of phase.steps) {
|
|
285
|
+
const stepRef = `${phase.id}.${step.id}`;
|
|
286
|
+
if (step.routingGroup) {
|
|
287
|
+
const route = routing.groups[step.routingGroup];
|
|
288
|
+
rows.push({
|
|
289
|
+
step: [...prefixSegments, stepRef].join(" > "),
|
|
290
|
+
group: routingGroupLabel(step.routingGroup),
|
|
291
|
+
executor: route.executor,
|
|
292
|
+
model: route.model,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (step.node !== "flow-run") {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const nestedFlowName = step.params?.fileName;
|
|
299
|
+
if (!nestedFlowName || !("const" in nestedFlowName) || typeof nestedFlowName.const !== "string") {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const nestedFlow = await loadNamedDeclarativeFlow(nestedFlowName.const, cwd, {
|
|
303
|
+
...(registryContext ? { registryContext } : {}),
|
|
304
|
+
});
|
|
305
|
+
const nestedFlowLabel = path.basename(nestedFlow.fileName, path.extname(nestedFlow.fileName));
|
|
306
|
+
rows.push(...await collectEffectiveRoutedStepRows(nestedFlow, cwd, routing, registryContext, [...prefixSegments, stepRef, nestedFlowLabel], nextAncestry));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return rows;
|
|
310
|
+
};
|
|
311
|
+
return run();
|
|
312
|
+
}
|
|
313
|
+
export async function describeEffectiveRoutingPreview(flowEntry, routing, cwd, registryContext) {
|
|
314
|
+
const previewGroups = await flowRoutingGroups(flowEntry, cwd, {
|
|
315
|
+
...(registryContext ? { registryContext } : {}),
|
|
316
|
+
});
|
|
317
|
+
const summaryRows = [
|
|
318
|
+
["Default", routing.defaultRoute.executor, routing.defaultRoute.model],
|
|
319
|
+
...previewGroups.map((group) => [
|
|
320
|
+
routingGroupLabel(group),
|
|
321
|
+
routing.groups[group].executor,
|
|
322
|
+
routing.groups[group].model,
|
|
323
|
+
]),
|
|
324
|
+
];
|
|
325
|
+
const lines = [
|
|
326
|
+
"Effective routes:",
|
|
327
|
+
...formatAsciiTable(["Scope", "Executor", "Model"], summaryRows, [18, 12, 36]),
|
|
328
|
+
];
|
|
329
|
+
const routedSteps = collapseRepeatedEffectiveRoutedStepRows(await collectEffectiveRoutedStepRows(flowEntry.flow, cwd, routing, registryContext));
|
|
330
|
+
if (routedSteps.length === 0) {
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
return [
|
|
334
|
+
...lines,
|
|
335
|
+
"",
|
|
336
|
+
"Routed LLM steps:",
|
|
337
|
+
...formatAsciiTable(["Step", "Group", "Executor", "Model"], routedSteps.map((row) => [row.step, row.group, row.executor, row.model]), [64, 16, 12, 30]),
|
|
338
|
+
].join("\n");
|
|
339
|
+
}
|
|
340
|
+
export async function requestInteractiveExecutionRouting(flowEntry, requestUserInput) {
|
|
341
|
+
const registryContext = await createPipelineRegistryContext(process.cwd());
|
|
342
|
+
const previewGroups = await flowRoutingGroups(flowEntry, process.cwd(), { registryContext });
|
|
343
|
+
const flowKey = flowRoutingKey(flowEntry);
|
|
344
|
+
const namedPresets = getNamedExecutionPresets();
|
|
345
|
+
const flowDefault = getFlowDefaultExecutionRouting(flowKey);
|
|
346
|
+
const lastUsed = getLastUsedExecutionRouting(flowKey);
|
|
347
|
+
const presetSelection = await requestUserInput(executionPresetSelectionForm(flowEntry, Boolean(flowDefault), Boolean(lastUsed), Object.keys(namedPresets).sort((left, right) => left.localeCompare(right, "en"))));
|
|
348
|
+
const selectedPresetValue = String(presetSelection.values.preset ?? "custom");
|
|
349
|
+
let selectedPreset;
|
|
350
|
+
let routing;
|
|
351
|
+
if (selectedPresetValue === "flow-default") {
|
|
352
|
+
if (!flowDefault) {
|
|
353
|
+
throw new TaskRunnerError("Flow default routing is unavailable.");
|
|
354
|
+
}
|
|
355
|
+
selectedPreset = { kind: "flow-default", label: "Flow default" };
|
|
356
|
+
routing = resolveStoredExecutionRoutingSnapshot(flowDefault.routing, registryContext.executors);
|
|
357
|
+
}
|
|
358
|
+
else if (selectedPresetValue === "last-used") {
|
|
359
|
+
if (!lastUsed) {
|
|
360
|
+
throw new TaskRunnerError("Last-used routing is unavailable.");
|
|
361
|
+
}
|
|
362
|
+
selectedPreset = { kind: "last-used", label: "Last used" };
|
|
363
|
+
routing = resolveStoredExecutionRoutingSnapshot(lastUsed.routing, registryContext.executors);
|
|
364
|
+
}
|
|
365
|
+
else if (selectedPresetValue.startsWith("named:")) {
|
|
366
|
+
const presetName = selectedPresetValue.slice("named:".length);
|
|
367
|
+
const namedPreset = namedPresets[presetName];
|
|
368
|
+
if (!namedPreset) {
|
|
369
|
+
throw new TaskRunnerError(`Named preset '${presetName}' is unavailable.`);
|
|
370
|
+
}
|
|
371
|
+
selectedPreset = { kind: "named", presetId: presetName, label: `Named preset: ${presetName}` };
|
|
372
|
+
routing = resolveStoredExecutionRoutingSnapshot(namedPreset.routing, registryContext.executors);
|
|
373
|
+
}
|
|
374
|
+
else if (selectedPresetValue.startsWith("built-in:")) {
|
|
375
|
+
const presetId = selectedPresetValue.slice("built-in:".length);
|
|
376
|
+
const preset = BUILT_IN_EXECUTION_PRESETS[presetId];
|
|
377
|
+
if (!preset) {
|
|
378
|
+
throw new TaskRunnerError(`Unknown execution preset '${presetId}'.`);
|
|
379
|
+
}
|
|
380
|
+
selectedPreset = { kind: "built-in", presetId, label: preset.label };
|
|
381
|
+
routing = resolveExecutionRouting({ presetId, executors: registryContext.executors });
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
selectedPreset = { kind: "custom", label: "Custom" };
|
|
385
|
+
routing = resolveExecutionRouting({
|
|
386
|
+
executors: registryContext.executors,
|
|
387
|
+
defaultRoute: {
|
|
388
|
+
executor: DEFAULT_LAUNCH_PROFILE.executor,
|
|
389
|
+
model: DEFAULT_LAUNCH_PROFILE.model,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
let editorDraft = routingEditorDraftFromRouting(routing);
|
|
394
|
+
let editorValidationMessage;
|
|
395
|
+
for (;;) {
|
|
396
|
+
const previewText = `Preset: ${selectedPreset.label}\n${await describeEffectiveRoutingPreview(flowEntry, routing, process.cwd(), registryContext)}`;
|
|
397
|
+
const actionResult = await requestUserInput(routingActionForm(previewText));
|
|
398
|
+
const action = String(actionResult.values.action ?? "start");
|
|
399
|
+
if (action === "cancel") {
|
|
400
|
+
throw new FlowInterruptedError("Flow launch cancelled.");
|
|
401
|
+
}
|
|
402
|
+
if (action === "start") {
|
|
403
|
+
saveLastUsedExecutionRouting(flowKey, routing, selectedPreset);
|
|
404
|
+
return { routing, selectedPreset };
|
|
405
|
+
}
|
|
406
|
+
const routingFormResult = await requestUserInput(advancedRoutingEditorForm(previewGroups, editorDraft, registryContext, editorValidationMessage));
|
|
407
|
+
const requestedDefaultRoute = {
|
|
408
|
+
executor: String(routingFormResult.values.default_route_executor ?? editorDraft.defaultRoute.executor),
|
|
409
|
+
model: String(routingFormResult.values.default_route_model ?? editorDraft.defaultRoute.model),
|
|
410
|
+
};
|
|
411
|
+
const requestedDraft = {
|
|
412
|
+
defaultRoute: requestedDefaultRoute,
|
|
413
|
+
groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
|
|
414
|
+
const submittedExecutor = String(routingFormResult.values[`${group}_executor`] ?? editorDraft.groups[group].executor);
|
|
415
|
+
const submittedModel = String(routingFormResult.values[`${group}_model`] ?? editorDraft.groups[group].model);
|
|
416
|
+
const inheritedBeforeEdit = editorDraft.groups[group].executor === editorDraft.defaultRoute.executor
|
|
417
|
+
&& editorDraft.groups[group].model === editorDraft.defaultRoute.model;
|
|
418
|
+
const groupRoute = inheritedBeforeEdit
|
|
419
|
+
&& submittedExecutor === editorDraft.groups[group].executor
|
|
420
|
+
&& submittedModel === editorDraft.groups[group].model
|
|
421
|
+
? requestedDefaultRoute
|
|
422
|
+
: { executor: submittedExecutor, model: submittedModel };
|
|
423
|
+
return [
|
|
424
|
+
group,
|
|
425
|
+
groupRoute,
|
|
426
|
+
];
|
|
427
|
+
})),
|
|
428
|
+
};
|
|
429
|
+
const normalizedDraft = normalizeEditableRoutingDraft(requestedDraft, registryContext.executors);
|
|
430
|
+
editorDraft = normalizedDraft.draft;
|
|
431
|
+
if (normalizedDraft.validationErrors.length > 0) {
|
|
432
|
+
editorValidationMessage = normalizedDraft.validationErrors.join("\n");
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
routing = resolveExecutionRouting({
|
|
437
|
+
executors: registryContext.executors,
|
|
438
|
+
defaultRoute: {
|
|
439
|
+
executor: normalizedDraft.draft.defaultRoute.executor,
|
|
440
|
+
model: normalizedDraft.draft.defaultRoute.model,
|
|
441
|
+
},
|
|
442
|
+
currentRunOverrides: normalizedDraft.currentRunOverrides,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (error instanceof TaskRunnerError) {
|
|
447
|
+
editorValidationMessage = error.message;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
editorDraft = routingEditorDraftFromRouting(routing);
|
|
453
|
+
editorValidationMessage = undefined;
|
|
454
|
+
selectedPreset = { kind: "custom", label: "Custom" };
|
|
455
|
+
const persistenceResult = await requestUserInput(routingPersistenceForm());
|
|
456
|
+
const persistence = String(persistenceResult.values.persistence ?? "current-run");
|
|
457
|
+
if (persistence === "flow-default") {
|
|
458
|
+
selectedPreset = { kind: "flow-default", label: "Flow default" };
|
|
459
|
+
saveFlowDefaultExecutionRouting(flowKey, routing, selectedPreset);
|
|
460
|
+
}
|
|
461
|
+
else if (persistence === "named-preset") {
|
|
462
|
+
const presetNameResult = await requestUserInput(presetNameForm());
|
|
463
|
+
const presetName = String(presetNameResult.values.name ?? "").trim();
|
|
464
|
+
if (!presetName) {
|
|
465
|
+
throw new TaskRunnerError("Preset name is required.");
|
|
466
|
+
}
|
|
467
|
+
selectedPreset = { kind: "named", presetId: presetName, label: `Named preset: ${presetName}` };
|
|
468
|
+
saveNamedExecutionPreset(presetName, routing, selectedPreset);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { designFile, designJsonFile, designReviewFile, designReviewJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, requireArtifacts, } from "../artifacts.js";
|
|
2
|
+
import { designFile, designJsonFile, designReviewFile, designReviewJsonFile, instantTaskInputJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, taskContextJsonFile, requireArtifacts, } from "../artifacts.js";
|
|
3
3
|
import { TaskRunnerError } from "../errors.js";
|
|
4
4
|
import { validateStructuredArtifacts } from "../structured-artifacts.js";
|
|
5
|
+
import { resolveLatestCompletedPlanningIteration } from "./planning-bundle.js";
|
|
5
6
|
const OPTIONAL_INPUT_NOT_PROVIDED = "not provided";
|
|
6
|
-
function resolveLatestDesignReviewIteration(taskKey) {
|
|
7
|
+
function resolveLatestDesignReviewIteration(taskKey, sourcePlanningIteration) {
|
|
8
|
+
if (sourcePlanningIteration !== undefined) {
|
|
9
|
+
const candidateMd = designReviewFile(taskKey, sourcePlanningIteration);
|
|
10
|
+
const candidateJson = designReviewJsonFile(taskKey, sourcePlanningIteration);
|
|
11
|
+
if (existsSync(candidateMd) && existsSync(candidateJson)) {
|
|
12
|
+
return sourcePlanningIteration;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
const latestMd = latestArtifactIteration(taskKey, "design-review", "md");
|
|
8
16
|
const latestJson = latestArtifactIteration(taskKey, "design-review", "json");
|
|
9
17
|
const maxIteration = Math.max(latestMd ?? 0, latestJson ?? 0);
|
|
@@ -22,33 +30,6 @@ function resolveLatestDesignReviewIteration(taskKey) {
|
|
|
22
30
|
requireArtifacts([fallbackMd, fallbackJson], "Plan-revise requires design-review markdown and JSON artifacts from the latest completed design-review run.");
|
|
23
31
|
throw new TaskRunnerError("Unreachable plan-revise design-review artifact resolution state.");
|
|
24
32
|
}
|
|
25
|
-
function resolveLatestCompletedPlanningIteration(taskKey) {
|
|
26
|
-
const latestDesignMd = latestArtifactIteration(taskKey, "design", "md") ?? 0;
|
|
27
|
-
const latestDesignJson = latestArtifactIteration(taskKey, "design", "json") ?? 0;
|
|
28
|
-
const latestPlanMd = latestArtifactIteration(taskKey, "plan", "md") ?? 0;
|
|
29
|
-
const latestPlanJson = latestArtifactIteration(taskKey, "plan", "json") ?? 0;
|
|
30
|
-
const maxIteration = Math.max(latestDesignMd, latestDesignJson, latestPlanMd, latestPlanJson);
|
|
31
|
-
for (let iteration = maxIteration; iteration >= 1; iteration -= 1) {
|
|
32
|
-
const paths = [
|
|
33
|
-
designFile(taskKey, iteration),
|
|
34
|
-
designJsonFile(taskKey, iteration),
|
|
35
|
-
planFile(taskKey, iteration),
|
|
36
|
-
planJsonFile(taskKey, iteration),
|
|
37
|
-
];
|
|
38
|
-
if (paths.every((candidate) => existsSync(candidate))) {
|
|
39
|
-
return iteration;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const fallbackIteration = maxIteration || 1;
|
|
43
|
-
const fallbackPaths = [
|
|
44
|
-
designFile(taskKey, fallbackIteration),
|
|
45
|
-
designJsonFile(taskKey, fallbackIteration),
|
|
46
|
-
planFile(taskKey, fallbackIteration),
|
|
47
|
-
planJsonFile(taskKey, fallbackIteration),
|
|
48
|
-
];
|
|
49
|
-
requireArtifacts(fallbackPaths, "Plan-revise requires design and plan markdown/JSON artifacts from the latest completed planning run.");
|
|
50
|
-
throw new TaskRunnerError("Unreachable plan-revise planning artifact resolution state.");
|
|
51
|
-
}
|
|
52
33
|
function resolveOptionalPromptFile(filePath) {
|
|
53
34
|
if (!existsSync(filePath)) {
|
|
54
35
|
return {
|
|
@@ -63,6 +44,14 @@ function resolveOptionalPromptFile(filePath) {
|
|
|
63
44
|
promptValue: filePath,
|
|
64
45
|
};
|
|
65
46
|
}
|
|
47
|
+
function resolveOptionalValidatedStructuredFile(filePath, schemaId, message) {
|
|
48
|
+
const resolved = resolveOptionalPromptFile(filePath);
|
|
49
|
+
if (!resolved.present) {
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
validateStructuredArtifacts([{ path: filePath, schemaId }], message);
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
66
55
|
function resolveOptionalQaPair(taskKey, iteration) {
|
|
67
56
|
const markdownPath = qaFile(taskKey, iteration);
|
|
68
57
|
const jsonPath = qaJsonFile(taskKey, iteration);
|
|
@@ -90,12 +79,15 @@ function resolveOptionalQaPair(taskKey, iteration) {
|
|
|
90
79
|
};
|
|
91
80
|
}
|
|
92
81
|
export function resolvePlanReviseInputContract(taskKey) {
|
|
93
|
-
const
|
|
82
|
+
const sourcePlanningIteration = resolveLatestCompletedPlanningIteration(taskKey, {
|
|
83
|
+
requireQa: false,
|
|
84
|
+
missingMessage: "Plan-revise requires design and plan markdown/JSON artifacts from the latest completed planning run.",
|
|
85
|
+
});
|
|
86
|
+
const reviewIteration = resolveLatestDesignReviewIteration(taskKey, sourcePlanningIteration);
|
|
94
87
|
const reviewMd = designReviewFile(taskKey, reviewIteration);
|
|
95
88
|
const reviewJson = designReviewJsonFile(taskKey, reviewIteration);
|
|
96
89
|
requireArtifacts([reviewMd, reviewJson], "Plan-revise requires design-review markdown and JSON artifacts.");
|
|
97
90
|
validateStructuredArtifacts([{ path: reviewJson, schemaId: "design-review/v1" }], "Plan-revise design-review structured artifact is invalid.");
|
|
98
|
-
const sourcePlanningIteration = resolveLatestCompletedPlanningIteration(taskKey);
|
|
99
91
|
const srcDesignMd = designFile(taskKey, sourcePlanningIteration);
|
|
100
92
|
const srcDesignJson = designJsonFile(taskKey, sourcePlanningIteration);
|
|
101
93
|
const srcPlanMd = planFile(taskKey, sourcePlanningIteration);
|
|
@@ -110,7 +102,12 @@ export function resolvePlanReviseInputContract(taskKey) {
|
|
|
110
102
|
const jiraTask = resolveOptionalPromptFile(jiraTaskFile(taskKey));
|
|
111
103
|
const jiraAttachmentsManifest = resolveOptionalPromptFile(jiraAttachmentsManifestFile(taskKey));
|
|
112
104
|
const jiraAttachmentsContext = resolveOptionalPromptFile(jiraAttachmentsContextFile(taskKey));
|
|
113
|
-
const planningAnswers =
|
|
105
|
+
const planningAnswers = resolveOptionalValidatedStructuredFile(planningAnswersJsonFile(taskKey), "user-input/v1", "Plan-revise planning answers structured artifact is invalid.");
|
|
106
|
+
const taskContextIteration = latestArtifactIteration(taskKey, "task-context", "json");
|
|
107
|
+
const taskContext = taskContextIteration === null
|
|
108
|
+
? { present: false, path: null, promptValue: OPTIONAL_INPUT_NOT_PROVIDED }
|
|
109
|
+
: resolveOptionalValidatedStructuredFile(taskContextJsonFile(taskKey, taskContextIteration), "task-context/v1", "Plan-revise task-context structured artifact is invalid.");
|
|
110
|
+
const taskInput = resolveOptionalValidatedStructuredFile(instantTaskInputJsonFile(taskKey), "user-input/v1", "Plan-revise instant-task input structured artifact is invalid.");
|
|
114
111
|
return {
|
|
115
112
|
reviewIteration,
|
|
116
113
|
reviewFile: reviewMd,
|
|
@@ -140,5 +137,11 @@ export function resolvePlanReviseInputContract(taskKey) {
|
|
|
140
137
|
hasPlanningAnswersJsonFile: planningAnswers.present,
|
|
141
138
|
planningAnswersJsonFilePath: planningAnswers.path,
|
|
142
139
|
planningAnswersJsonFile: planningAnswers.promptValue,
|
|
140
|
+
hasTaskContextJsonFile: taskContext.present,
|
|
141
|
+
taskContextJsonFilePath: taskContext.path,
|
|
142
|
+
taskContextJsonFile: taskContext.promptValue,
|
|
143
|
+
hasTaskInputJsonFile: taskInput.present,
|
|
144
|
+
taskInputJsonFilePath: taskInput.path,
|
|
145
|
+
taskInputJsonFile: taskInput.promptValue,
|
|
143
146
|
};
|
|
144
147
|
}
|