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.
Files changed (110) hide show
  1. package/README.md +76 -19
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +88 -3
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/executors.js +2 -2
  6. package/dist/doctor/checks/flow-readiness.js +15 -18
  7. package/dist/flow-state.js +212 -15
  8. package/dist/index.js +539 -209
  9. package/dist/interactive/blessed-session.js +361 -0
  10. package/dist/interactive/controller.js +1326 -0
  11. package/dist/interactive/create-interactive-session.js +5 -0
  12. package/dist/interactive/ink/index.js +597 -0
  13. package/dist/interactive/progress.js +245 -0
  14. package/dist/interactive/selectors.js +14 -0
  15. package/dist/interactive/session.js +1 -0
  16. package/dist/interactive/state.js +34 -0
  17. package/dist/interactive/tree.js +155 -0
  18. package/dist/interactive/types.js +1 -0
  19. package/dist/interactive/view-model.js +1 -0
  20. package/dist/interactive-ui.js +159 -194
  21. package/dist/pipeline/auto-flow.js +9 -6
  22. package/dist/pipeline/context.js +7 -5
  23. package/dist/pipeline/declarative-flow-runner.js +212 -6
  24. package/dist/pipeline/declarative-flows.js +63 -17
  25. package/dist/pipeline/execution-routing-config.js +15 -0
  26. package/dist/pipeline/flow-catalog.js +50 -12
  27. package/dist/pipeline/flow-run-resume.js +29 -0
  28. package/dist/pipeline/flow-specs/auto-common.json +90 -360
  29. package/dist/pipeline/flow-specs/auto-golang.json +81 -360
  30. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  31. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  32. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  33. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +316 -0
  34. package/dist/pipeline/flow-specs/design-review.json +10 -0
  35. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  36. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  37. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  38. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  39. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  40. package/dist/pipeline/flow-specs/implement.json +13 -6
  41. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  42. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  43. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  44. package/dist/pipeline/flow-specs/plan.json +51 -71
  45. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  46. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  47. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  48. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  49. package/dist/pipeline/flow-specs/review/review.json +37 -31
  50. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  51. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  52. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  53. package/dist/pipeline/launch-profile-config.js +30 -18
  54. package/dist/pipeline/node-contract.js +1 -0
  55. package/dist/pipeline/node-registry.js +115 -6
  56. package/dist/pipeline/node-runner.js +3 -2
  57. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  58. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  59. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  60. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  61. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  62. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  63. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  64. package/dist/pipeline/nodes/flow-run-node.js +242 -8
  65. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  66. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  67. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  68. package/dist/pipeline/nodes/llm-prompt-node.js +38 -36
  69. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  70. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  71. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  72. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  73. package/dist/pipeline/nodes/user-input-node.js +38 -3
  74. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  75. package/dist/pipeline/plugin-loader.js +389 -0
  76. package/dist/pipeline/plugin-types.js +1 -0
  77. package/dist/pipeline/prompt-registry.js +3 -1
  78. package/dist/pipeline/prompt-runtime.js +4 -1
  79. package/dist/pipeline/registry.js +71 -4
  80. package/dist/pipeline/review-iteration.js +26 -0
  81. package/dist/pipeline/spec-compiler.js +3 -0
  82. package/dist/pipeline/spec-loader.js +14 -0
  83. package/dist/pipeline/spec-types.js +3 -0
  84. package/dist/pipeline/spec-validator.js +20 -0
  85. package/dist/pipeline/value-resolver.js +76 -2
  86. package/dist/plugin-sdk.js +1 -0
  87. package/dist/prompts.js +36 -14
  88. package/dist/review-severity.js +45 -0
  89. package/dist/runtime/artifact-registry.js +405 -0
  90. package/dist/runtime/design-review-input-contract.js +17 -16
  91. package/dist/runtime/env-loader.js +3 -0
  92. package/dist/runtime/execution-routing-store.js +134 -0
  93. package/dist/runtime/execution-routing.js +233 -0
  94. package/dist/runtime/interactive-execution-routing.js +471 -0
  95. package/dist/runtime/plan-revise-input-contract.js +35 -32
  96. package/dist/runtime/planning-bundle.js +123 -0
  97. package/dist/runtime/ready-to-merge.js +22 -1
  98. package/dist/runtime/review-input-contract.js +100 -0
  99. package/dist/structured-artifact-schema-registry.js +9 -0
  100. package/dist/structured-artifact-schemas.json +140 -1
  101. package/dist/structured-artifacts.js +77 -6
  102. package/dist/user-input.js +70 -3
  103. package/docs/example/.flows/examples/claude-example.json +50 -0
  104. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  105. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  106. package/docs/examples/.flows/claude-example.json +50 -0
  107. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  108. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  109. package/docs/plugin-sdk.md +731 -0
  110. 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 reviewIteration = resolveLatestDesignReviewIteration(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);
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 = resolveOptionalPromptFile(planningAnswersJsonFile(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.");
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
  }