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.
Files changed (100) hide show
  1. package/README.md +29 -7
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +21 -1
  4. package/dist/doctor/checks/cwd-context.js +4 -3
  5. package/dist/doctor/checks/env-diagnostics.js +193 -71
  6. package/dist/doctor/checks/flow-readiness.js +212 -203
  7. package/dist/doctor/index.js +1 -1
  8. package/dist/doctor/orchestrator.js +18 -7
  9. package/dist/doctor/runner.js +9 -8
  10. package/dist/doctor/types.js +12 -0
  11. package/dist/flow-state.js +75 -15
  12. package/dist/index.js +499 -199
  13. package/dist/interactive/blessed-session.js +361 -0
  14. package/dist/interactive/controller.js +1293 -0
  15. package/dist/interactive/create-interactive-session.js +5 -0
  16. package/dist/interactive/ink/index.js +576 -0
  17. package/dist/interactive/progress.js +245 -0
  18. package/dist/interactive/selectors.js +14 -0
  19. package/dist/interactive/session.js +1 -0
  20. package/dist/interactive/state.js +34 -0
  21. package/dist/interactive/tree.js +155 -0
  22. package/dist/interactive/types.js +1 -0
  23. package/dist/interactive/view-model.js +1 -0
  24. package/dist/interactive-ui.js +159 -194
  25. package/dist/pipeline/context.js +1 -0
  26. package/dist/pipeline/declarative-flow-runner.js +212 -6
  27. package/dist/pipeline/declarative-flows.js +27 -0
  28. package/dist/pipeline/execution-routing-config.js +15 -0
  29. package/dist/pipeline/flow-catalog.js +23 -3
  30. package/dist/pipeline/flow-run-resume.js +29 -0
  31. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  32. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  33. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  34. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  35. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  36. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  37. package/dist/pipeline/flow-specs/design-review.json +249 -0
  38. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  39. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  40. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  41. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  42. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  43. package/dist/pipeline/flow-specs/implement.json +24 -5
  44. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  45. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  46. package/dist/pipeline/flow-specs/plan-revise.json +267 -0
  47. package/dist/pipeline/flow-specs/plan.json +48 -70
  48. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  49. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  50. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  51. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  52. package/dist/pipeline/flow-specs/review/review.json +37 -31
  53. package/dist/pipeline/flow-specs/task-describe.json +62 -2
  54. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  55. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  56. package/dist/pipeline/node-registry.js +49 -1
  57. package/dist/pipeline/node-runner.js +3 -2
  58. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  59. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  60. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  61. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  62. package/dist/pipeline/nodes/ensure-summary-json-node.js +70 -0
  63. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  64. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  65. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  66. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  67. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  68. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  69. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  70. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  71. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  72. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  73. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  74. package/dist/pipeline/nodes/user-input-node.js +38 -3
  75. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  76. package/dist/pipeline/prompt-registry.js +5 -1
  77. package/dist/pipeline/prompt-runtime.js +4 -1
  78. package/dist/pipeline/review-iteration.js +26 -0
  79. package/dist/pipeline/spec-compiler.js +2 -0
  80. package/dist/pipeline/spec-types.js +5 -0
  81. package/dist/pipeline/spec-validator.js +14 -0
  82. package/dist/pipeline/value-resolver.js +84 -1
  83. package/dist/prompts.js +82 -13
  84. package/dist/review-severity.js +45 -0
  85. package/dist/runtime/artifact-registry.js +402 -0
  86. package/dist/runtime/design-review-input-contract.js +113 -0
  87. package/dist/runtime/env-loader.js +3 -0
  88. package/dist/runtime/execution-routing-store.js +134 -0
  89. package/dist/runtime/execution-routing.js +227 -0
  90. package/dist/runtime/interactive-execution-routing.js +462 -0
  91. package/dist/runtime/plan-revise-input-contract.js +147 -0
  92. package/dist/runtime/planning-bundle.js +123 -0
  93. package/dist/runtime/ready-to-merge.js +31 -0
  94. package/dist/runtime/review-input-contract.js +100 -0
  95. package/dist/scope.js +11 -2
  96. package/dist/structured-artifact-schema-registry.js +10 -0
  97. package/dist/structured-artifact-schemas.json +257 -1
  98. package/dist/structured-artifacts.js +83 -6
  99. package/dist/user-input.js +70 -3
  100. package/package.json +6 -3
@@ -0,0 +1,113 @@
1
+ import { existsSync } from "node:fs";
2
+ import { designFile, designJsonFile, instantTaskInputJsonFile, jiraAttachmentsContextFile, jiraAttachmentsManifestFile, jiraTaskFile, latestArtifactIteration, planFile, planJsonFile, planningAnswersJsonFile, qaFile, qaJsonFile, taskContextJsonFile, requireArtifacts, } from "../artifacts.js";
3
+ import { validateStructuredArtifacts } from "../structured-artifacts.js";
4
+ import { resolveLatestCompletedPlanningIteration } from "./planning-bundle.js";
5
+ const OPTIONAL_INPUT_NOT_PROVIDED = "not provided";
6
+ function requiredPlanningArtifactPaths(taskKey, iteration) {
7
+ return {
8
+ designFile: designFile(taskKey, iteration),
9
+ designJsonFile: designJsonFile(taskKey, iteration),
10
+ planFile: planFile(taskKey, iteration),
11
+ planJsonFile: planJsonFile(taskKey, iteration),
12
+ };
13
+ }
14
+ function resolveOptionalPromptFile(filePath) {
15
+ if (!existsSync(filePath)) {
16
+ return {
17
+ present: false,
18
+ path: null,
19
+ promptValue: OPTIONAL_INPUT_NOT_PROVIDED,
20
+ };
21
+ }
22
+ return {
23
+ present: true,
24
+ path: filePath,
25
+ promptValue: filePath,
26
+ };
27
+ }
28
+ function resolveOptionalValidatedStructuredFile(filePath, schemaId, message) {
29
+ const resolved = resolveOptionalPromptFile(filePath);
30
+ if (!resolved.present) {
31
+ return resolved;
32
+ }
33
+ validateStructuredArtifacts([{ path: filePath, schemaId }], message);
34
+ return resolved;
35
+ }
36
+ function resolveOptionalQaPair(taskKey, iteration) {
37
+ const markdownPath = qaFile(taskKey, iteration);
38
+ const jsonPath = qaJsonFile(taskKey, iteration);
39
+ const markdownExists = existsSync(markdownPath);
40
+ const jsonExists = existsSync(jsonPath);
41
+ if (!markdownExists && !jsonExists) {
42
+ return {
43
+ hasQaArtifacts: false,
44
+ qaFilePath: null,
45
+ qaJsonFilePath: null,
46
+ qaFile: OPTIONAL_INPUT_NOT_PROVIDED,
47
+ qaJsonFile: OPTIONAL_INPUT_NOT_PROVIDED,
48
+ };
49
+ }
50
+ if (!markdownExists || !jsonExists) {
51
+ requireArtifacts([markdownPath, jsonPath], "Design-review accepts QA artifacts only as a complete markdown/JSON pair for the selected planning iteration.");
52
+ }
53
+ validateStructuredArtifacts([{ path: jsonPath, schemaId: "qa-plan/v1" }], "Design-review QA structured artifact is invalid.");
54
+ return {
55
+ hasQaArtifacts: true,
56
+ qaFilePath: markdownPath,
57
+ qaJsonFilePath: jsonPath,
58
+ qaFile: markdownPath,
59
+ qaJsonFile: jsonPath,
60
+ };
61
+ }
62
+ /**
63
+ * Resolves the full design-review input contract from artifacts already present in the task scope.
64
+ * Required planning artifacts must come from one completed planning iteration. Optional contextual
65
+ * inputs are exposed as stable prompt values and explicit presence flags so the flow can remain
66
+ * deterministic when some context is absent.
67
+ */
68
+ export function resolveDesignReviewInputContract(taskKey) {
69
+ const planningIteration = resolveLatestCompletedPlanningIteration(taskKey, {
70
+ requireQa: false,
71
+ missingMessage: "Design-review requires design and plan markdown/JSON artifacts from the latest completed planning run.",
72
+ });
73
+ const requiredArtifacts = requiredPlanningArtifactPaths(taskKey, planningIteration);
74
+ requireArtifacts(Object.values(requiredArtifacts), "Design-review requires design and plan markdown/JSON artifacts from the latest completed planning run.");
75
+ validateStructuredArtifacts([
76
+ { path: requiredArtifacts.designJsonFile, schemaId: "implementation-design/v1" },
77
+ { path: requiredArtifacts.planJsonFile, schemaId: "implementation-plan/v1" },
78
+ ], "Design-review required planning structured artifacts are invalid.");
79
+ const qaArtifacts = resolveOptionalQaPair(taskKey, planningIteration);
80
+ const jiraTask = resolveOptionalPromptFile(jiraTaskFile(taskKey));
81
+ const jiraAttachmentsManifest = resolveOptionalPromptFile(jiraAttachmentsManifestFile(taskKey));
82
+ const jiraAttachmentsContext = resolveOptionalPromptFile(jiraAttachmentsContextFile(taskKey));
83
+ const planningAnswers = resolveOptionalValidatedStructuredFile(planningAnswersJsonFile(taskKey), "user-input/v1", "Design-review planning answers structured artifact is invalid.");
84
+ const taskContextIteration = latestArtifactIteration(taskKey, "task-context", "json");
85
+ const taskContext = taskContextIteration === null
86
+ ? { present: false, path: null, promptValue: OPTIONAL_INPUT_NOT_PROVIDED }
87
+ : resolveOptionalValidatedStructuredFile(taskContextJsonFile(taskKey, taskContextIteration), "task-context/v1", "Design-review task-context structured artifact is invalid.");
88
+ const taskInput = resolveOptionalValidatedStructuredFile(instantTaskInputJsonFile(taskKey), "user-input/v1", "Design-review instant-task input structured artifact is invalid.");
89
+ return {
90
+ planningIteration,
91
+ ...requiredArtifacts,
92
+ ...qaArtifacts,
93
+ hasJiraTaskFile: jiraTask.present,
94
+ jiraTaskFilePath: jiraTask.path,
95
+ jiraTaskFile: jiraTask.promptValue,
96
+ hasJiraAttachmentsManifestFile: jiraAttachmentsManifest.present,
97
+ jiraAttachmentsManifestFilePath: jiraAttachmentsManifest.path,
98
+ jiraAttachmentsManifestFile: jiraAttachmentsManifest.promptValue,
99
+ hasJiraAttachmentsContextFile: jiraAttachmentsContext.present,
100
+ jiraAttachmentsContextFilePath: jiraAttachmentsContext.path,
101
+ jiraAttachmentsContextFile: jiraAttachmentsContext.promptValue,
102
+ hasPlanningAnswersJsonFile: planningAnswers.present,
103
+ planningAnswersJsonFilePath: planningAnswers.path,
104
+ planningAnswersJsonFile: planningAnswers.promptValue,
105
+ hasTaskContextJsonFile: taskContext.present,
106
+ taskContextJsonFilePath: taskContext.path,
107
+ taskContextJsonFile: taskContext.promptValue,
108
+ hasTaskInputJsonFile: taskInput.present,
109
+ taskInputJsonFilePath: taskInput.path,
110
+ taskInputJsonFile: taskInput.promptValue,
111
+ };
112
+ }
113
+ export { OPTIONAL_INPUT_NOT_PROVIDED };
@@ -35,6 +35,9 @@ function globalConfigDir() {
35
35
  function ensureGlobalConfigDir() {
36
36
  mkdirSync(globalConfigDir(), { recursive: true });
37
37
  }
38
+ export function agentweaverConfigDir() {
39
+ return globalConfigDir();
40
+ }
38
41
  export function loadTieredEnv(projectDir) {
39
42
  ensureGlobalConfigDir();
40
43
  const shellEnvKeys = new Set(Object.keys(process.env));
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { TaskRunnerError } from "../errors.js";
4
+ import { resolveStoredExecutionRoutingSnapshot } from "./execution-routing.js";
5
+ import { agentweaverConfigDir } from "./env-loader.js";
6
+ const EXECUTION_ROUTING_STORE_VERSION = 1;
7
+ function storePath() {
8
+ return path.join(agentweaverConfigDir(), "execution-routing.json");
9
+ }
10
+ function nowIso8601() {
11
+ return new Date().toISOString();
12
+ }
13
+ function emptyStore() {
14
+ return {
15
+ version: EXECUTION_ROUTING_STORE_VERSION,
16
+ namedPresets: {},
17
+ flowDefaults: {},
18
+ lastUsedByFlow: {},
19
+ };
20
+ }
21
+ function validateSelectedPreset(value, pathLabel) {
22
+ if (!value || typeof value !== "object") {
23
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.`);
24
+ }
25
+ const candidate = value;
26
+ if (typeof candidate.kind !== "string" || typeof candidate.label !== "string") {
27
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.`);
28
+ }
29
+ if (candidate.kind === "built-in" || candidate.kind === "named") {
30
+ if (typeof candidate.presetId !== "string" || candidate.presetId.trim().length === 0) {
31
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.presetId.`);
32
+ }
33
+ return {
34
+ kind: candidate.kind,
35
+ presetId: candidate.presetId,
36
+ label: candidate.label,
37
+ };
38
+ }
39
+ if (candidate.kind === "flow-default" || candidate.kind === "last-used" || candidate.kind === "custom") {
40
+ return {
41
+ kind: candidate.kind,
42
+ label: candidate.label,
43
+ };
44
+ }
45
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.selectedPreset.kind.`);
46
+ }
47
+ function validateRoutingEntry(value, pathLabel) {
48
+ if (!value || typeof value !== "object") {
49
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.`);
50
+ }
51
+ const candidate = value;
52
+ if (typeof candidate.updatedAt !== "string") {
53
+ throw new TaskRunnerError(`Invalid execution routing store entry at ${pathLabel}.updatedAt.`);
54
+ }
55
+ return {
56
+ routing: resolveStoredExecutionRoutingSnapshot(candidate.routing),
57
+ selectedPreset: validateSelectedPreset(candidate.selectedPreset, pathLabel),
58
+ updatedAt: candidate.updatedAt,
59
+ };
60
+ }
61
+ function validateEntryMap(value, pathLabel) {
62
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
63
+ throw new TaskRunnerError(`Invalid execution routing store section '${pathLabel}'.`);
64
+ }
65
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, validateRoutingEntry(entry, `${pathLabel}.${key}`)]));
66
+ }
67
+ function validateStore(raw) {
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
69
+ throw new TaskRunnerError("Execution routing store must be a JSON object.");
70
+ }
71
+ const candidate = raw;
72
+ if (candidate.version !== EXECUTION_ROUTING_STORE_VERSION) {
73
+ throw new TaskRunnerError(`Unsupported execution routing store version: ${String(candidate.version ?? "unknown")}.`);
74
+ }
75
+ return {
76
+ version: EXECUTION_ROUTING_STORE_VERSION,
77
+ namedPresets: validateEntryMap(candidate.namedPresets ?? {}, "namedPresets"),
78
+ flowDefaults: validateEntryMap(candidate.flowDefaults ?? {}, "flowDefaults"),
79
+ lastUsedByFlow: validateEntryMap(candidate.lastUsedByFlow ?? {}, "lastUsedByFlow"),
80
+ };
81
+ }
82
+ export function loadExecutionRoutingStore() {
83
+ const filePath = storePath();
84
+ if (!existsSync(filePath)) {
85
+ return emptyStore();
86
+ }
87
+ try {
88
+ return validateStore(JSON.parse(readFileSync(filePath, "utf8")));
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ throw new TaskRunnerError(`Failed to load execution routing store ${filePath}: ${message}. Delete or repair the file and try again.`);
93
+ }
94
+ }
95
+ export function saveExecutionRoutingStore(store) {
96
+ const filePath = storePath();
97
+ mkdirSync(path.dirname(filePath), { recursive: true });
98
+ const tempFilePath = `${filePath}.tmp`;
99
+ writeFileSync(`${tempFilePath}`, `${JSON.stringify(store, null, 2)}\n`, "utf8");
100
+ renameSync(tempFilePath, filePath);
101
+ }
102
+ function withUpdatedAt(entry) {
103
+ return {
104
+ ...entry,
105
+ updatedAt: nowIso8601(),
106
+ };
107
+ }
108
+ export function saveNamedExecutionPreset(name, routing, selectedPreset) {
109
+ const store = loadExecutionRoutingStore();
110
+ store.namedPresets[name] = withUpdatedAt({ routing, selectedPreset });
111
+ saveExecutionRoutingStore(store);
112
+ }
113
+ export function saveFlowDefaultExecutionRouting(flowKey, routing, selectedPreset) {
114
+ const store = loadExecutionRoutingStore();
115
+ store.flowDefaults[flowKey] = withUpdatedAt({ routing, selectedPreset });
116
+ saveExecutionRoutingStore(store);
117
+ }
118
+ export function saveLastUsedExecutionRouting(flowKey, routing, selectedPreset) {
119
+ const store = loadExecutionRoutingStore();
120
+ store.lastUsedByFlow[flowKey] = withUpdatedAt({ routing, selectedPreset });
121
+ saveExecutionRoutingStore(store);
122
+ }
123
+ export function getNamedExecutionPresets() {
124
+ return loadExecutionRoutingStore().namedPresets;
125
+ }
126
+ export function getFlowDefaultExecutionRouting(flowKey) {
127
+ return loadExecutionRoutingStore().flowDefaults[flowKey] ?? null;
128
+ }
129
+ export function getLastUsedExecutionRouting(flowKey) {
130
+ return loadExecutionRoutingStore().lastUsedByFlow[flowKey] ?? null;
131
+ }
132
+ export function executionRoutingStoreFile() {
133
+ return storePath();
134
+ }
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+ import { TaskRunnerError } from "../errors.js";
3
+ import { ALLOWED_MODELS_BY_EXECUTOR, DEFAULT_LAUNCH_PROFILE, defaultModelForExecutor, isAllowedModelForExecutor, resolveLaunchProfile, } from "../pipeline/launch-profile-config.js";
4
+ import { BUILT_IN_EXECUTION_PRESET_IDS, EXECUTION_ROUTING_GROUPS, } from "../pipeline/execution-routing-config.js";
5
+ export const BUILT_IN_EXECUTION_PRESETS = {
6
+ balanced: {
7
+ id: "balanced",
8
+ label: "Balanced",
9
+ description: "Use Codex for planning and review, OpenCode for implementation-style loops.",
10
+ defaultRoute: { executor: "opencode", model: "default" },
11
+ groupOverrides: {
12
+ planning: { executor: "codex", model: "gpt-5.4" },
13
+ "design-review": { executor: "codex", model: "gpt-5.4" },
14
+ review: { executor: "codex", model: "gpt-5.4" },
15
+ },
16
+ },
17
+ "quality-first": {
18
+ id: "quality-first",
19
+ label: "Quality-first",
20
+ description: "Run all routing groups on Codex GPT-5.4.",
21
+ defaultRoute: { executor: "codex", model: "gpt-5.4" },
22
+ },
23
+ "cheap-first": {
24
+ id: "cheap-first",
25
+ label: "Cheap-first",
26
+ description: "Prefer lower-cost models while preserving valid executor and model pairs.",
27
+ defaultRoute: { executor: "opencode", model: "opencode/minimax-m2.5-free" },
28
+ groupOverrides: {
29
+ planning: { executor: "codex", model: "gpt-5.4-mini" },
30
+ "design-review": { executor: "codex", model: "gpt-5.4-mini" },
31
+ review: { executor: "codex", model: "gpt-5.4-mini" },
32
+ },
33
+ },
34
+ "codex-only": {
35
+ id: "codex-only",
36
+ label: "Codex-only",
37
+ description: "Run all routing groups on Codex with the default Codex model.",
38
+ defaultRoute: { executor: "codex", model: "default" },
39
+ },
40
+ "opencode-only": {
41
+ id: "opencode-only",
42
+ label: "OpenCode-only",
43
+ description: "Run all routing groups on OpenCode with the default OpenCode model.",
44
+ defaultRoute: { executor: "opencode", model: "default" },
45
+ },
46
+ };
47
+ function stableRoutingPayload(routing) {
48
+ return JSON.stringify({
49
+ defaultRoute: {
50
+ executor: routing.defaultRoute.executor,
51
+ model: routing.defaultRoute.model,
52
+ },
53
+ groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [
54
+ group,
55
+ {
56
+ executor: routing.groups[group].executor,
57
+ model: routing.groups[group].model,
58
+ },
59
+ ])),
60
+ });
61
+ }
62
+ export function executionRoutingFingerprint(routing) {
63
+ return createHash("sha256").update(stableRoutingPayload({ ...routing, fingerprint: "" })).digest("hex");
64
+ }
65
+ export function validateExecutionRoute(executor, model) {
66
+ const allowedModels = ALLOWED_MODELS_BY_EXECUTOR[executor];
67
+ if (!allowedModels) {
68
+ throw new TaskRunnerError(`Unsupported llm executor '${executor}'.`);
69
+ }
70
+ if (!allowedModels.includes(model)) {
71
+ throw new TaskRunnerError(`Model '${model}' is not allowed for executor '${executor}'.`);
72
+ }
73
+ }
74
+ export function toExecutionRouteSelection(route) {
75
+ return {
76
+ executor: route.executor,
77
+ model: route.model,
78
+ };
79
+ }
80
+ export function resolveExecutionRoute(selection, fallback) {
81
+ const launchProfile = resolveLaunchProfile({
82
+ executor: selection.executor,
83
+ model: selection.model,
84
+ }, fallback);
85
+ validateExecutionRoute(launchProfile.executor, launchProfile.model);
86
+ return launchProfile;
87
+ }
88
+ export function resolveExecutionRouting(options) {
89
+ const fallbackDefaultRoute = options.fallbackDefaultRoute ?? DEFAULT_LAUNCH_PROFILE;
90
+ const preset = options.presetId ? BUILT_IN_EXECUTION_PRESETS[options.presetId] : null;
91
+ const defaultRouteSelection = options.defaultRoute ?? preset?.defaultRoute ?? toExecutionRouteSelection(fallbackDefaultRoute);
92
+ const defaultRoute = resolveExecutionRoute(defaultRouteSelection, fallbackDefaultRoute);
93
+ const groups = Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
94
+ const override = options.currentRunOverrides?.[group]
95
+ ?? options.presetOverrides?.[group]
96
+ ?? preset?.groupOverrides?.[group]
97
+ ?? { executor: "default", model: "default" };
98
+ return [group, resolveExecutionRoute(override, defaultRoute)];
99
+ }));
100
+ const fingerprint = executionRoutingFingerprint({
101
+ defaultRoute,
102
+ groups,
103
+ });
104
+ return {
105
+ defaultRoute,
106
+ groups,
107
+ fingerprint,
108
+ };
109
+ }
110
+ export function routingGroupLabel(group) {
111
+ switch (group) {
112
+ case "planning":
113
+ return "Planning";
114
+ case "design-review":
115
+ return "Design review";
116
+ case "implementation":
117
+ return "Implementation";
118
+ case "review":
119
+ return "Review";
120
+ case "repair-loop":
121
+ return "Repair loop";
122
+ case "local-fix-loop":
123
+ return "Local fix loop";
124
+ default:
125
+ return group;
126
+ }
127
+ }
128
+ export function describeExecutionRouting(routing, groups = EXECUTION_ROUTING_GROUPS) {
129
+ const lines = [`Default: ${routing.defaultRoute.executor} / ${routing.defaultRoute.model}`];
130
+ for (const group of groups) {
131
+ const route = routing.groups[group];
132
+ lines.push(`${routingGroupLabel(group)}: ${route.executor} / ${route.model}`);
133
+ }
134
+ return lines.join("\n");
135
+ }
136
+ export function executorsForRoutingGroups(routing, groups) {
137
+ const requiredExecutors = [];
138
+ for (const group of groups) {
139
+ const executor = routing.groups[group].executor;
140
+ if (!requiredExecutors.includes(executor)) {
141
+ requiredExecutors.push(executor);
142
+ }
143
+ }
144
+ return requiredExecutors;
145
+ }
146
+ export function normalizeEditableExecutionRouting(routes) {
147
+ const normalizedRoutes = {};
148
+ const validationErrors = [];
149
+ for (const group of EXECUTION_ROUTING_GROUPS) {
150
+ const route = routes[group];
151
+ if (isAllowedModelForExecutor(route.executor, route.model)) {
152
+ normalizedRoutes[group] = { ...route };
153
+ continue;
154
+ }
155
+ normalizedRoutes[group] = {
156
+ executor: route.executor,
157
+ model: defaultModelForExecutor(route.executor),
158
+ };
159
+ validationErrors.push(`${routingGroupLabel(group)} model '${route.model}' is not allowed for executor '${route.executor}'. Select a ${route.executor} model.`);
160
+ }
161
+ return {
162
+ routes: normalizedRoutes,
163
+ validationErrors,
164
+ };
165
+ }
166
+ export function builtInExecutionPresetList() {
167
+ return BUILT_IN_EXECUTION_PRESET_IDS.map((id) => BUILT_IN_EXECUTION_PRESETS[id]);
168
+ }
169
+ export function selectedExecutionPresetLabel(selection) {
170
+ return selection.label;
171
+ }
172
+ export function cloneResolvedExecutionRouting(routing) {
173
+ return {
174
+ defaultRoute: { ...routing.defaultRoute },
175
+ groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [group, { ...routing.groups[group] }])),
176
+ fingerprint: routing.fingerprint,
177
+ };
178
+ }
179
+ export function defaultExecutionRouting() {
180
+ return resolveExecutionRouting({
181
+ defaultRoute: {
182
+ executor: DEFAULT_LAUNCH_PROFILE.executor,
183
+ model: DEFAULT_LAUNCH_PROFILE.model,
184
+ },
185
+ });
186
+ }
187
+ export function resolveStoredExecutionRoutingSnapshot(routing) {
188
+ validateExecutionRoute(routing.defaultRoute.executor, routing.defaultRoute.model);
189
+ for (const group of EXECUTION_ROUTING_GROUPS) {
190
+ validateExecutionRoute(routing.groups[group].executor, routing.groups[group].model);
191
+ }
192
+ const fingerprint = executionRoutingFingerprint({
193
+ defaultRoute: routing.defaultRoute,
194
+ groups: routing.groups,
195
+ });
196
+ return {
197
+ defaultRoute: {
198
+ ...routing.defaultRoute,
199
+ selectedExecutor: routing.defaultRoute.selectedExecutor ?? routing.defaultRoute.executor,
200
+ selectedModel: routing.defaultRoute.selectedModel ?? routing.defaultRoute.model,
201
+ fingerprint: routing.defaultRoute.fingerprint ?? `${routing.defaultRoute.executor}::${routing.defaultRoute.model}`,
202
+ },
203
+ groups: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => {
204
+ const route = routing.groups[group];
205
+ return [group, {
206
+ ...route,
207
+ selectedExecutor: route.selectedExecutor ?? route.executor,
208
+ selectedModel: route.selectedModel ?? route.model,
209
+ fingerprint: route.fingerprint ?? `${route.executor}::${route.model}`,
210
+ }];
211
+ })),
212
+ fingerprint,
213
+ };
214
+ }
215
+ export function singleLaunchProfileExecutionRouting(launchProfile) {
216
+ return resolveExecutionRouting({
217
+ defaultRoute: toExecutionRouteSelection(launchProfile),
218
+ currentRunOverrides: Object.fromEntries(EXECUTION_ROUTING_GROUPS.map((group) => [group, toExecutionRouteSelection(launchProfile)])),
219
+ });
220
+ }
221
+ export function modelOptionsForExecutor(executor) {
222
+ const defaultModel = defaultModelForExecutor(executor);
223
+ return ALLOWED_MODELS_BY_EXECUTOR[executor].map((model) => ({
224
+ value: model,
225
+ label: model === defaultModel ? `${model} [default]` : model,
226
+ }));
227
+ }