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