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
package/dist/index.js CHANGED
@@ -1,43 +1,57 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
- import { REVIEW_FILE_RE, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, latestArtifactIteration, nextArtifactIteration, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewAssessmentFile, reviewAssessmentJsonFile, reviewFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, flowStateFile, taskSummaryFile, } from "./artifacts.js";
6
+ import { bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designReviewFile, designReviewJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, instantTaskInputJsonFile, latestArtifactIteration, nextArtifactIteration, readyToMergeFile, requireArtifacts, reviewAssessmentFile, reviewAssessmentJsonFile, reviewFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, flowStateFile, taskSummaryFile, } from "./artifacts.js";
7
7
  import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
8
8
  import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, rewindFlowRunStateToPhase, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
9
9
  import { requireJiraTaskFile } from "./jira.js";
10
10
  import { validateStructuredArtifacts } from "./structured-artifacts.js";
11
+ import { AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV, parseReviewSeverityCsv, resolveReviewBlockingSeveritiesFromEnv, } from "./review-severity.js";
11
12
  import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
12
13
  import { runNodeChecks } from "./pipeline/checks.js";
13
14
  import { createPipelineContext } from "./pipeline/context.js";
14
- import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
15
+ import { collectFlowRoutingGroups, loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
15
16
  import { runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
16
- import { findCatalogEntry, isBuiltInCommandFlowId, loadInteractiveFlowCatalog, toDeclarativeFlowRef } from "./pipeline/flow-catalog.js";
17
- import { ALLOWED_MODELS_BY_EXECUTOR, defaultModelForExecutor, DEFAULT_LAUNCH_PROFILE, LLM_EXECUTOR_IDS, resolveLaunchProfile, } from "./pipeline/launch-profile-config.js";
17
+ import { builtInCommandFlowFile, findCatalogEntry, flowRoutingGroups, isBuiltInCommandFlowId, loadInteractiveFlowCatalog, toDeclarativeFlowRef, } from "./pipeline/flow-catalog.js";
18
+ import { DEFAULT_LAUNCH_PROFILE, } from "./pipeline/launch-profile-config.js";
19
+ import { withCanonicalReviewLoopParams } from "./pipeline/review-iteration.js";
18
20
  import { evaluateCondition, resolveValue } from "./pipeline/value-resolver.js";
19
21
  import { resolveCmd } from "./runtime/command-resolution.js";
20
22
  import { loadTieredEnv } from "./runtime/env-loader.js";
21
23
  import { agentweaverHome } from "./runtime/agentweaver-home.js";
22
24
  import { runCommand } from "./runtime/process-runner.js";
23
- import { InteractiveUi } from "./interactive-ui.js";
25
+ import { createArtifactRegistry } from "./runtime/artifact-registry.js";
26
+ import { resolveDesignReviewInputContract } from "./runtime/design-review-input-contract.js";
27
+ import { resolvePlanReviseInputContract } from "./runtime/plan-revise-input-contract.js";
28
+ import { resolveLatestPlanningBundle } from "./runtime/planning-bundle.js";
29
+ import { inspectReviewInputContract, resolveReviewInputContract } from "./runtime/review-input-contract.js";
30
+ import { clearReadyToMergeFile } from "./runtime/ready-to-merge.js";
31
+ import { describeExecutionRouting, executorsForRoutingGroups, resolveExecutionRouting, } from "./runtime/execution-routing.js";
32
+ import { requestInteractiveExecutionRouting } from "./runtime/interactive-execution-routing.js";
33
+ import { createInteractiveSession } from "./interactive/create-interactive-session.js";
24
34
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
25
35
  import { requestUserInputInTerminal } from "./user-input.js";
26
36
  import { runDoctorCommand } from "./doctor/index.js";
27
- import { attachJiraContext, detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
37
+ import { detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
28
38
  const COMMANDS = [
29
39
  "auto-golang",
30
40
  "auto-common",
41
+ "auto-simple",
31
42
  "auto-status",
32
43
  "auto-reset",
33
44
  "bug-analyze",
34
45
  "bug-fix",
46
+ "design-review",
35
47
  "doctor",
36
48
  "git-commit",
37
49
  "gitlab-diff-review",
38
50
  "gitlab-review",
51
+ "instant-task",
39
52
  "mr-description",
40
53
  "plan",
54
+ "plan-revise",
41
55
  "task-describe",
42
56
  "implement",
43
57
  "review",
@@ -52,6 +66,7 @@ function createRuntimeServices(signal) {
52
66
  return {
53
67
  resolveCmd,
54
68
  runCommand: (argv, options = {}) => runCommand(argv, { ...options, ...(signal ? { signal } : {}) }),
69
+ artifactRegistry: createArtifactRegistry(),
55
70
  };
56
71
  }
57
72
  const runtimeServices = createRuntimeServices();
@@ -99,19 +114,26 @@ function usage() {
99
114
  agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
100
115
  agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
101
116
  agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
117
+ agentweaver design-review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
102
118
  agentweaver doctor [<category>|<check-id>] [--json]
119
+ agentweaver instant-task [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>]
103
120
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
104
121
  agentweaver plan [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [<jira-browse-url|jira-issue-key>]
122
+ agentweaver plan-revise [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
105
123
  agentweaver task-describe [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
106
124
  agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
107
- agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
108
- agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
109
- agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
125
+ agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
126
+ agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
127
+ agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
110
128
  agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
111
129
  agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
112
130
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
113
131
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
114
132
  agentweaver auto-golang --help-phases
133
+ agentweaver auto-common [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
134
+ agentweaver auto-common --help-phases
135
+ agentweaver auto-simple [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
136
+ agentweaver auto-simple --help-phases
115
137
  agentweaver auto-status [<jira-browse-url|jira-issue-key>]
116
138
  agentweaver auto-reset [<jira-browse-url|jira-issue-key>]
117
139
 
@@ -125,8 +147,9 @@ Flags:
125
147
  --force In interactive mode, regenerate task summary in Jira-backed flows
126
148
  --dry Fetch Jira task, but print codex/opencode commands instead of executing them
127
149
  --verbose Show live stdout/stderr of launched commands
128
- --scope Explicit workflow scope name for non-Jira runs
150
+ --scope Explicit workflow scope name for non-Jira runs except instant-task
129
151
  --prompt Extra prompt text appended to the base prompt
152
+ --blocking-severities Comma-separated severities that block merge and drive review-fix auto-selection
130
153
  --md-lang Language for markdown output files: en (English) or ru (Russian, default)
131
154
 
132
155
  Required environment variables:
@@ -138,6 +161,7 @@ Optional environment variables:
138
161
  JIRA_BASE_URL
139
162
  GITLAB_TOKEN
140
163
  AGENTWEAVER_HOME
164
+ ${AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV}
141
165
  CODEX_BIN
142
166
  CODEX_MODEL
143
167
  OPENCODE_BIN
@@ -145,8 +169,11 @@ Optional environment variables:
145
169
 
146
170
  Notes:
147
171
  - Jira-backed task flows will ask for Jira task via user-input when it is not passed as an argument. task-describe can also work from a manual task description without Jira.
172
+ - instant-task always uses the current branch-derived project scope and rejects explicit scope overrides or Jira arguments.
148
173
  - All flow state and artifacts are stored in the current project scope by default.
149
- - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.`;
174
+ - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.
175
+ - ${AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV} sets the default blocking severities. Default: blocker,critical,high.
176
+ - Interactive mode requires Ink runtime dependencies and a real TTY.`;
150
177
  }
151
178
  function packageVersion() {
152
179
  const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
@@ -176,7 +203,11 @@ function buildFlowResumeDetails(state) {
176
203
  `Current step: ${currentStep}`,
177
204
  `Updated: ${state.updatedAt}`,
178
205
  ];
179
- if (state.launchProfile) {
206
+ if (state.executionRouting) {
207
+ lines.push(`Default route: ${state.executionRouting.defaultRoute.executor} / ${state.executionRouting.defaultRoute.model}`);
208
+ lines.push(`Routing fingerprint: ${state.executionRouting.fingerprint}`);
209
+ }
210
+ else if (state.launchProfile) {
180
211
  lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
181
212
  }
182
213
  if (state.lastError) {
@@ -184,79 +215,6 @@ function buildFlowResumeDetails(state) {
184
215
  }
185
216
  return lines.join("\n");
186
217
  }
187
- function launchProfileSelectionForm() {
188
- const defaultExecutor = DEFAULT_LAUNCH_PROFILE.executor;
189
- return {
190
- formId: "flow-launch-profile",
191
- title: "LLM Launch Settings",
192
- description: `Select an executor for the flow. Current default: ${defaultExecutor}.`,
193
- submitLabel: "Continue",
194
- fields: [
195
- {
196
- id: "executor",
197
- type: "single-select",
198
- label: "Executor",
199
- required: true,
200
- default: defaultExecutor,
201
- options: LLM_EXECUTOR_IDS.map((id) => ({
202
- value: id,
203
- label: id === defaultExecutor ? `${id} [default]` : id,
204
- })),
205
- },
206
- ],
207
- };
208
- }
209
- function launchModelSelectionForm(executor) {
210
- const resolvedExecutor = executor === "default" ? DEFAULT_LAUNCH_PROFILE.executor : executor;
211
- const defaultModel = defaultModelForExecutor(resolvedExecutor);
212
- const options = ALLOWED_MODELS_BY_EXECUTOR[resolvedExecutor].map((model) => ({
213
- value: model,
214
- label: model === defaultModel ? `${model} [default]` : model,
215
- }));
216
- return {
217
- formId: "flow-launch-model",
218
- title: "LLM Launch Settings",
219
- description: `Select a model for the flow. Current default for ${resolvedExecutor}: ${defaultModel}.`,
220
- submitLabel: "Start",
221
- fields: [
222
- {
223
- id: "model",
224
- type: "single-select",
225
- label: "Model",
226
- required: true,
227
- default: defaultModel,
228
- options,
229
- },
230
- ],
231
- };
232
- }
233
- function isFormCancellation(error, formId) {
234
- return error instanceof TaskRunnerError && error.message === `User cancelled form '${formId}'.`;
235
- }
236
- async function requestInteractiveLaunchProfile(requestUserInput) {
237
- for (;;) {
238
- const executorFormResult = await requestUserInput(launchProfileSelectionForm());
239
- const rawExecutor = String(executorFormResult.values.executor ?? DEFAULT_LAUNCH_PROFILE.executor);
240
- const executor = LLM_EXECUTOR_IDS.find((id) => id === rawExecutor);
241
- if (!executor) {
242
- throw new TaskRunnerError(`Unsupported launch executor '${rawExecutor}'.`);
243
- }
244
- try {
245
- const modelFormResult = await requestUserInput(launchModelSelectionForm(executor));
246
- const rawModel = String(modelFormResult.values.model ?? defaultModelForExecutor(executor)).trim();
247
- return resolveLaunchProfile({
248
- executor,
249
- model: rawModel.length > 0 ? rawModel : defaultModelForExecutor(executor),
250
- }, DEFAULT_LAUNCH_PROFILE);
251
- }
252
- catch (error) {
253
- if (isFormCancellation(error, "flow-launch-model")) {
254
- continue;
255
- }
256
- throw error;
257
- }
258
- }
259
- }
260
218
  function buildResolverContext(pipelineContext, flowParams, flowConstants, repeatVars, executionState) {
261
219
  return {
262
220
  flowParams,
@@ -330,13 +288,21 @@ function validateDeclarativePhaseResumeState(phase, phaseState, pipelineContext,
330
288
  }
331
289
  }
332
290
  }
333
- function validateDeclarativeFlowResumeState(flowEntry, config, state, launchProfile, runtime = runtimeServices) {
334
- if (state.launchProfile) {
335
- if (!launchProfile) {
336
- throw new TaskRunnerError("Resume is impossible because launch profile is missing. Use restart.");
291
+ function validateDeclarativeFlowResumeState(flowEntry, config, state, executionRouting, runtime = runtimeServices) {
292
+ if (state.flowId === "auto-common") {
293
+ const persistedPhaseIds = state.executionState.phases.map((p) => p.id);
294
+ const hasLegacyPlanningGatePhases = persistedPhaseIds.some((id) => ["design_review", "verdict", "plan_revision", "design_review_repeat", "verdict_repeat"].includes(id));
295
+ if (hasLegacyPlanningGatePhases) {
296
+ throw new TaskRunnerError("Resume is impossible because the persisted state was created with the legacy phase graph. Use restart.");
297
+ }
298
+ }
299
+ const persistedFingerprint = state.routingFingerprint ?? state.executionRouting?.fingerprint ?? state.launchProfile?.fingerprint;
300
+ if (persistedFingerprint) {
301
+ if (!executionRouting) {
302
+ throw new TaskRunnerError("Resume is impossible because execution routing is missing. Use restart.");
337
303
  }
338
- if (state.launchProfile.fingerprint !== launchProfile.fingerprint) {
339
- throw new TaskRunnerError(`Resume is impossible because launch profile changed (${state.launchProfile.executor}/${state.launchProfile.model} -> ${launchProfile.executor}/${launchProfile.model}). Use restart.`);
304
+ if (persistedFingerprint !== executionRouting.fingerprint) {
305
+ throw new TaskRunnerError("Resume is impossible because execution routing changed. Use restart.");
340
306
  }
341
307
  }
342
308
  if (flowRequiresTaskScope(flowEntry) && !config.jiraRef) {
@@ -350,8 +316,9 @@ function validateDeclarativeFlowResumeState(flowEntry, config, state, launchProf
350
316
  ...(config.mdLang !== undefined ? { mdLang: config.mdLang } : {}),
351
317
  runtime,
352
318
  requestUserInput: requestUserInputInTerminal,
319
+ ...(executionRouting ? { executionRouting } : {}),
353
320
  });
354
- const flowParams = defaultDeclarativeFlowParams(config, false, launchProfile ? { launchProfile } : {});
321
+ const flowParams = defaultDeclarativeFlowParams(config, false, executionRouting ? { executionRouting, launchProfile: executionRouting.defaultRoute } : {});
355
322
  for (const phase of flowEntry.flow.phases) {
356
323
  const phaseState = state.executionState.phases.find((candidate) => candidate.id === phase.id);
357
324
  if (!phaseState) {
@@ -364,19 +331,21 @@ function scopeWithRestoredJiraContext(scope, state) {
364
331
  if (scope.jiraRef || !state?.jiraRef?.trim()) {
365
332
  return scope;
366
333
  }
367
- return attachJiraContext(scope, state.jiraRef);
334
+ return resolveProjectScope(null, state.jiraRef);
335
+ }
336
+ function buildInteractiveBaseConfig(flowId, scope) {
337
+ return buildBaseConfig(flowId, {
338
+ ...(flowId !== "instant-task" && scope.jiraRef ? { jiraRef: scope.jiraRef } : {}),
339
+ });
368
340
  }
369
341
  function lookupInteractiveFlowResume(flowEntry, currentScope) {
370
342
  const directState = loadFlowRunState(currentScope.scopeKey, flowEntry.id);
371
343
  if (directState && hasResumableFlowState(directState)) {
372
344
  try {
373
345
  const effectiveScope = scopeWithRestoredJiraContext(currentScope, directState);
374
- const baseConfig = buildBaseConfig(flowEntry.id, {
375
- ...(effectiveScope.jiraRef ? { jiraRef: effectiveScope.jiraRef } : {}),
376
- scopeName: effectiveScope.scopeKey,
377
- });
346
+ const baseConfig = buildInteractiveBaseConfig(flowEntry.id, effectiveScope);
378
347
  const config = buildRuntimeConfig(baseConfig, effectiveScope);
379
- validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.launchProfile);
348
+ validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.executionRouting);
380
349
  return {
381
350
  resumeAvailable: true,
382
351
  hasExistingState: true,
@@ -409,22 +378,19 @@ function printAutoCommonPhasesHelp() {
409
378
  phaseLines.push("", "You can run auto-common with:", "agentweaver auto-common <jira>");
410
379
  printPanel("Auto-Common Phases", phaseLines.join("\n"), "magenta");
411
380
  }
381
+ function autoSimplePhaseIds() {
382
+ return loadDeclarativeFlow({ source: "built-in", fileName: "auto-simple.json" }).phases.map((phase) => phase.id);
383
+ }
384
+ function printAutoSimplePhasesHelp() {
385
+ const phaseLines = ["Available auto-simple phases:", "", ...autoSimplePhaseIds()];
386
+ phaseLines.push("", "You can run auto-simple with:", "agentweaver auto-simple <jira>");
387
+ printPanel("Auto-Simple Phases", phaseLines.join("\n"), "magenta");
388
+ }
412
389
  function nextReviewIterationForTask(taskKey) {
413
- let maxIndex = 0;
414
- const workspaceDir = scopeWorkspaceDir(taskKey);
415
- if (!existsSync(workspaceDir)) {
416
- return 1;
417
- }
418
- for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
419
- if (!entry.isFile()) {
420
- continue;
421
- }
422
- const match = REVIEW_FILE_RE.exec(entry.name);
423
- if (match && match[1] === taskKey) {
424
- maxIndex = Math.max(maxIndex, Number.parseInt(match[2] ?? "0", 10));
425
- }
426
- }
427
- return maxIndex + 1;
390
+ return nextArtifactIteration(taskKey, "review");
391
+ }
392
+ function nextDesignReviewIterationForTask(taskKey) {
393
+ return nextArtifactIteration(taskKey, "design-review");
428
394
  }
429
395
  function buildBaseConfig(command, options = {}) {
430
396
  return {
@@ -432,6 +398,7 @@ function buildBaseConfig(command, options = {}) {
432
398
  jiraRef: options.jiraRef ?? null,
433
399
  scopeName: options.scopeName ?? null,
434
400
  reviewFixPoints: options.reviewFixPoints ?? null,
401
+ reviewBlockingSeverities: options.reviewBlockingSeverities ?? resolveReviewBlockingSeveritiesFromEnv(),
435
402
  extraPrompt: options.extraPrompt ?? null,
436
403
  autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
437
404
  mdLang: options.mdLang ?? null,
@@ -441,19 +408,23 @@ function buildBaseConfig(command, options = {}) {
441
408
  };
442
409
  }
443
410
  function commandRequiresTask(command) {
444
- return (command === "plan" ||
411
+ return (command === "plan-revise" ||
445
412
  command === "bug-analyze" ||
446
413
  command === "bug-fix" ||
414
+ command === "design-review" ||
447
415
  command === "mr-description" ||
448
416
  command === "auto-golang" ||
449
417
  command === "auto-common" ||
418
+ command === "auto-simple" ||
450
419
  command === "auto-status" ||
451
420
  command === "auto-reset");
452
421
  }
453
422
  function commandSupportsProjectScope(command) {
454
- return (command === "git-commit" ||
423
+ return (command === "plan" ||
424
+ command === "git-commit" ||
455
425
  command === "gitlab-diff-review" ||
456
426
  command === "gitlab-review" ||
427
+ command === "instant-task" ||
457
428
  command === "task-describe" ||
458
429
  command === "implement" ||
459
430
  command === "review" ||
@@ -463,9 +434,21 @@ function commandSupportsProjectScope(command) {
463
434
  command === "run-go-linter-loop");
464
435
  }
465
436
  async function resolveScopeForCommand(config, requestUserInput) {
437
+ if (config.command === "instant-task") {
438
+ if (config.scopeName?.trim()) {
439
+ throw new TaskRunnerError("Command 'instant-task' rejects explicit scope overrides. The current branch-derived scope is the only supported lineage identity.");
440
+ }
441
+ if (config.jiraRef?.trim()) {
442
+ throw new TaskRunnerError("Command 'instant-task' does not accept a Jira task argument. Start it without a positional Jira reference.");
443
+ }
444
+ return resolveProjectScope();
445
+ }
466
446
  if (config.jiraRef?.trim()) {
467
447
  return resolveProjectScope(config.scopeName, config.jiraRef);
468
448
  }
449
+ if (config.command === "plan") {
450
+ return resolveProjectScope(config.scopeName);
451
+ }
469
452
  if (commandRequiresTask(config.command)) {
470
453
  try {
471
454
  const jiraContext = await requestJiraContext(requestUserInput);
@@ -496,27 +479,61 @@ function buildRuntimeConfig(baseConfig, scope) {
496
479
  ...(scope.jiraTaskFile ? { jiraTaskFile: scope.jiraTaskFile } : {}),
497
480
  };
498
481
  }
499
- function checkPrerequisites(config) {
500
- if (config.command === "bug-analyze" ||
501
- config.command === "bug-fix" ||
502
- config.command === "mr-description" ||
503
- config.command === "plan" ||
504
- config.command === "task-describe" ||
505
- config.command === "review" ||
506
- config.command === "run-go-tests-loop" ||
507
- config.command === "run-go-linter-loop") {
482
+ function routingForPrerequisites(launchProfile, executionRouting) {
483
+ if (executionRouting) {
484
+ return executionRouting;
485
+ }
486
+ return resolveExecutionRouting({
487
+ defaultRoute: launchProfile
488
+ ? {
489
+ executor: launchProfile.executor,
490
+ model: launchProfile.model,
491
+ }
492
+ : {
493
+ executor: DEFAULT_LAUNCH_PROFILE.executor,
494
+ model: DEFAULT_LAUNCH_PROFILE.model,
495
+ },
496
+ });
497
+ }
498
+ function flowSpecFileForPrerequisiteChecks(command) {
499
+ return isBuiltInCommandFlowId(command) ? builtInCommandFlowFile(command) : null;
500
+ }
501
+ function commandRoutingGroupsForPrerequisiteChecks(command, cwd) {
502
+ const fileName = flowSpecFileForPrerequisiteChecks(command);
503
+ if (!fileName) {
504
+ return [];
505
+ }
506
+ return collectFlowRoutingGroups(loadDeclarativeFlow({ source: "built-in", fileName }), cwd);
507
+ }
508
+ function resolveExecutorPrerequisite(executor) {
509
+ if (executor === "codex") {
508
510
  resolveCmd("codex", "CODEX_BIN");
511
+ return;
512
+ }
513
+ resolveCmd("opencode", "OPENCODE_BIN");
514
+ }
515
+ function checkPrerequisites(config, launchProfile, executionRouting) {
516
+ const routing = routingForPrerequisites(launchProfile, executionRouting);
517
+ const groups = commandRoutingGroupsForPrerequisiteChecks(config.command, process.cwd());
518
+ for (const executor of executorsForRoutingGroups(routing, groups)) {
519
+ resolveExecutorPrerequisite(executor);
509
520
  }
510
521
  }
511
- function checkAutoPrerequisites(config) {
512
- resolveCmd("codex", "CODEX_BIN");
522
+ function checkAutoPrerequisites(config, launchProfile, executionRouting) {
523
+ checkPrerequisites(config, launchProfile, executionRouting);
513
524
  }
514
525
  function autoFlowParams(config, forceRefreshSummary = false) {
515
526
  return {
516
527
  jiraApiUrl: config.jiraApiUrl,
517
528
  taskKey: config.taskKey,
529
+ taskContextIteration: nextArtifactIteration(config.taskKey, "task-context", "json"),
530
+ taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
531
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
532
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
533
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
518
534
  extraPrompt: config.extraPrompt,
519
535
  reviewFixPoints: config.reviewFixPoints,
536
+ reviewBlockingSeverities: config.reviewBlockingSeverities,
520
537
  forceRefresh: forceRefreshSummary,
521
538
  mdLang: config.mdLang,
522
539
  runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
@@ -525,6 +542,72 @@ function autoFlowParams(config, forceRefreshSummary = false) {
525
542
  runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
526
543
  };
527
544
  }
545
+ function reviewFlowParamsFromContract(config) {
546
+ const contract = resolveReviewInputContract(config.taskKey);
547
+ return {
548
+ taskKey: config.taskKey,
549
+ planningIteration: contract.planningIteration,
550
+ designFile: contract.designFile,
551
+ designJsonFile: contract.designJsonFile,
552
+ planFile: contract.planFile,
553
+ planJsonFile: contract.planJsonFile,
554
+ hasTaskContextJsonFile: contract.hasTaskContextJsonFile,
555
+ taskContextJsonFilePath: contract.taskContextJsonFilePath,
556
+ taskContextJsonFile: contract.taskContextJsonFile,
557
+ hasJiraTaskFile: contract.hasJiraTaskFile,
558
+ jiraTaskFilePath: contract.jiraTaskFilePath,
559
+ jiraTaskFile: contract.jiraTaskFile,
560
+ hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
561
+ taskInputJsonFilePath: contract.taskInputJsonFilePath,
562
+ taskInputJsonFile: contract.taskInputJsonFile,
563
+ };
564
+ }
565
+ function hasStructuredReviewInputs(taskKey) {
566
+ const inspection = inspectReviewInputContract(taskKey);
567
+ if (inspection.status === "ready") {
568
+ return true;
569
+ }
570
+ if (inspection.status === "missing-planning") {
571
+ return false;
572
+ }
573
+ throw new TaskRunnerError(`Structured review requires a normalized task-context artifact, or legacy Jira/instant-task context, in scope '${taskKey}'.`);
574
+ }
575
+ function latestTaskContextIteration(taskKey) {
576
+ const iteration = latestArtifactIteration(taskKey, "task-context", "json");
577
+ if (iteration === null) {
578
+ throw new TaskRunnerError(`Plan mode requires a normalized task-context artifact in scope '${taskKey}'.`);
579
+ }
580
+ return iteration;
581
+ }
582
+ function loadInstantTaskInputDefaults(taskKey) {
583
+ const artifactPath = instantTaskInputJsonFile(taskKey);
584
+ if (!existsSync(artifactPath)) {
585
+ return null;
586
+ }
587
+ try {
588
+ validateStructuredArtifacts([{ path: artifactPath, schemaId: "user-input/v1" }], "Instant-task source input structured artifact is invalid.");
589
+ const parsed = JSON.parse(readFileSync(artifactPath, "utf8"));
590
+ const values = parsed.values;
591
+ if (!values || typeof values !== "object" || Array.isArray(values)) {
592
+ return null;
593
+ }
594
+ const normalizedEntries = [];
595
+ for (const [key, value] of Object.entries(values)) {
596
+ if (typeof value === "string" || typeof value === "boolean") {
597
+ normalizedEntries.push([key, value]);
598
+ continue;
599
+ }
600
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
601
+ normalizedEntries.push([key, [...value]]);
602
+ continue;
603
+ }
604
+ }
605
+ return Object.fromEntries(normalizedEntries);
606
+ }
607
+ catch {
608
+ return null;
609
+ }
610
+ }
528
611
  function interactiveFlowDefinition(entry) {
529
612
  const flow = entry.flow;
530
613
  return {
@@ -592,12 +675,14 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
592
675
  runtime,
593
676
  ...(setSummary ? { setSummary } : {}),
594
677
  requestUserInput,
678
+ ...(overrides.executionRouting ? { executionRouting: overrides.executionRouting } : {}),
595
679
  });
596
680
  const flow = loadDeclarativeFlow(flowRef);
597
681
  const initialExecutionState = {
598
682
  flowKind: flow.kind,
599
683
  flowVersion: flow.version,
600
684
  terminated: false,
685
+ terminationOutcome: "success",
601
686
  phases: [],
602
687
  };
603
688
  let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
@@ -609,7 +694,10 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
609
694
  absolutePath: flow.absolutePath,
610
695
  treePath: [],
611
696
  flow,
612
- }, config, persistedState, overrides.launchProfile, runtime);
697
+ }, config, persistedState, overrides.executionRouting ?? (overrides.launchProfile ? resolveExecutionRouting({ defaultRoute: {
698
+ executor: overrides.launchProfile.executor,
699
+ model: overrides.launchProfile.model,
700
+ } }) : undefined), runtime);
613
701
  persistedState = prepareFlowStateForResume(persistedState);
614
702
  }
615
703
  else if (launchMode === "restart") {
@@ -617,10 +705,18 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
617
705
  }
618
706
  const executionState = persistedState?.executionState ?? initialExecutionState;
619
707
  const state = persistedState
620
- ?? createFlowRunState(config.scope.scopeKey, flowId, executionState, config.jiraRef, overrides.launchProfile);
621
- if (overrides.launchProfile) {
708
+ ?? createFlowRunState(config.scope.scopeKey, flowId, executionState, config.jiraRef, overrides.launchProfile, overrides.executionRouting, overrides.selectedRoutingPreset);
709
+ if (overrides.executionRouting) {
710
+ state.executionRouting = overrides.executionRouting;
711
+ state.routingFingerprint = overrides.executionRouting.fingerprint;
712
+ state.launchProfile = overrides.executionRouting.defaultRoute;
713
+ }
714
+ else if (overrides.launchProfile) {
622
715
  state.launchProfile = overrides.launchProfile;
623
716
  }
717
+ if (overrides.selectedRoutingPreset) {
718
+ state.selectedRoutingPreset = overrides.selectedRoutingPreset;
719
+ }
624
720
  state.status = "running";
625
721
  state.lastError = null;
626
722
  state.currentStep = findCurrentFlowExecutionStep(state);
@@ -645,7 +741,12 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
645
741
  },
646
742
  });
647
743
  }
648
- state.status = "completed";
744
+ if (executionState.terminated) {
745
+ state.status = executionState.terminationOutcome === "success" ? "completed" : "blocked";
746
+ }
747
+ else {
748
+ state.status = "completed";
749
+ }
649
750
  state.currentStep = null;
650
751
  state.lastError = null;
651
752
  state.executionState = executionState;
@@ -671,12 +772,24 @@ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, overri
671
772
  ...defaultDeclarativeFlowParams(config, false, overrides),
672
773
  ...flowParams,
673
774
  };
674
- await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, mergedFlowParams, overrides, requestUserInput, setSummary, launchMode, runtime);
775
+ await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, withCanonicalReviewLoopParams(loadDeclarativeFlow({ source: "built-in", fileName }).kind, mergedFlowParams), overrides, requestUserInput, setSummary, launchMode, runtime);
675
776
  }
676
777
  function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overrides = {}) {
677
778
  const iteration = nextReviewIterationForTask(config.taskKey);
678
779
  const latestIteration = latestArtifactIteration(config.taskKey, "review");
679
- const launchProfile = overrides.launchProfile ?? resolveLaunchProfile({ executor: "default", model: "default" }, DEFAULT_LAUNCH_PROFILE);
780
+ const latestTaskContext = latestArtifactIteration(config.taskKey, "task-context", "json");
781
+ const executionRouting = overrides.executionRouting ?? resolveExecutionRouting({
782
+ defaultRoute: overrides.launchProfile
783
+ ? {
784
+ executor: overrides.launchProfile.executor,
785
+ model: overrides.launchProfile.model,
786
+ }
787
+ : {
788
+ executor: DEFAULT_LAUNCH_PROFILE.executor,
789
+ model: DEFAULT_LAUNCH_PROFILE.model,
790
+ },
791
+ });
792
+ const launchProfile = executionRouting.defaultRoute;
680
793
  return {
681
794
  taskKey: config.taskKey,
682
795
  jiraRef: config.jiraRef,
@@ -687,12 +800,16 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
687
800
  workspaceDir: scopeWorkspaceDir(config.taskKey),
688
801
  extraPrompt: config.extraPrompt,
689
802
  reviewFixPoints: config.reviewFixPoints,
803
+ reviewBlockingSeverities: config.reviewBlockingSeverities,
690
804
  mdLang: config.mdLang,
691
805
  llmExecutor: launchProfile.executor,
692
806
  llmModel: launchProfile.model,
693
807
  launchProfile,
808
+ executionRouting,
694
809
  iteration,
810
+ baseIteration: iteration,
695
811
  latestIteration,
812
+ taskContextIteration: latestTaskContext ?? nextArtifactIteration(config.taskKey, "task-context", "json"),
696
813
  taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
697
814
  designIteration: nextArtifactIteration(config.taskKey, "design"),
698
815
  planIteration: nextArtifactIteration(config.taskKey, "plan"),
@@ -738,20 +855,50 @@ function requireJiraConfig(config) {
738
855
  throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
739
856
  }
740
857
  }
741
- async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", launchProfile, runtime = runtimeServices) {
858
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", launchProfile, executionRouting, selectedRoutingPreset, runtime = runtimeServices) {
742
859
  if (baseConfig.command === "doctor") {
743
860
  const exitCode = await runDoctorCommand(baseConfig.doctorArgs ?? []);
744
861
  return exitCode === 0;
745
862
  }
746
863
  const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
864
+ const flowOverrides = executionRouting
865
+ ? {
866
+ launchProfile: executionRouting.defaultRoute,
867
+ executionRouting,
868
+ ...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
869
+ }
870
+ : launchProfile
871
+ ? { launchProfile }
872
+ : {};
873
+ if (config.command === "instant-task") {
874
+ checkPrerequisites(config, launchProfile, executionRouting);
875
+ const hasPersistedInstantTaskState = loadFlowRunState(config.scope.scopeKey, "instant-task") !== null;
876
+ const repromptInstantTaskInput = launchMode === "restart"
877
+ && hasPersistedInstantTaskState
878
+ && requestUserInput !== requestUserInputInTerminal;
879
+ await runDeclarativeFlowBySpecFile("instant-task.json", config, {
880
+ taskKey: config.taskKey,
881
+ taskContextIteration: nextArtifactIteration(config.taskKey, "task-context", "json"),
882
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
883
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
884
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
885
+ extraPrompt: config.extraPrompt,
886
+ mdLang: config.mdLang,
887
+ repromptInstantTaskInput,
888
+ ...(repromptInstantTaskInput
889
+ ? { prefilledInstantTaskInputValues: loadInstantTaskInputDefaults(config.taskKey) ?? undefined }
890
+ : {}),
891
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
892
+ return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
893
+ }
747
894
  if (config.command === "auto-golang") {
748
895
  requireJiraConfig(config);
749
- checkAutoPrerequisites(config);
750
896
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
751
897
  process.env.JIRA_API_URL = config.jiraApiUrl;
752
898
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
753
899
  let effectiveLaunchMode = launchMode;
754
900
  let effectiveLaunchProfile = launchProfile;
901
+ let effectiveExecutionRouting = executionRouting;
755
902
  if (config.autoFromPhase) {
756
903
  const flow = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
757
904
  const persistedState = loadFlowRunState(config.scope.scopeKey, "auto-golang");
@@ -762,18 +909,37 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
762
909
  saveFlowRunState(persistedState);
763
910
  effectiveLaunchMode = "resume";
764
911
  effectiveLaunchProfile ??= persistedState.launchProfile;
912
+ effectiveExecutionRouting ??= persistedState.executionRouting;
765
913
  printPanel("Auto-Golang Resume", `Auto-golang pipeline will continue from phase: ${config.autoFromPhase}`, "yellow");
766
914
  }
767
- await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveLaunchProfile ? { launchProfile: effectiveLaunchProfile } : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
915
+ checkAutoPrerequisites(config, effectiveLaunchProfile, effectiveExecutionRouting);
916
+ await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveExecutionRouting
917
+ ? {
918
+ launchProfile: effectiveExecutionRouting.defaultRoute,
919
+ executionRouting: effectiveExecutionRouting,
920
+ ...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
921
+ }
922
+ : effectiveLaunchProfile
923
+ ? { launchProfile: effectiveLaunchProfile }
924
+ : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
768
925
  return false;
769
926
  }
770
927
  if (config.command === "auto-common") {
771
928
  requireJiraConfig(config);
772
- checkAutoPrerequisites(config);
929
+ checkAutoPrerequisites(config, launchProfile, executionRouting);
930
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
931
+ process.env.JIRA_API_URL = config.jiraApiUrl;
932
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile;
933
+ await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
934
+ return false;
935
+ }
936
+ if (config.command === "auto-simple") {
937
+ requireJiraConfig(config);
938
+ checkAutoPrerequisites(config, launchProfile, executionRouting);
773
939
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
774
940
  process.env.JIRA_API_URL = config.jiraApiUrl;
775
941
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
776
- await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
942
+ await runDeclarativeFlowBySpecFile("auto-simple.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
777
943
  return false;
778
944
  }
779
945
  if (config.command === "auto-status") {
@@ -790,7 +956,11 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
790
956
  `Current step: ${currentStep}`,
791
957
  `Updated: ${state.updatedAt}`,
792
958
  ];
793
- if (state.launchProfile) {
959
+ if (state.executionRouting) {
960
+ lines.push(`Default route: ${state.executionRouting.defaultRoute.executor} / ${state.executionRouting.defaultRoute.model}`);
961
+ lines.push(`Routing fingerprint: ${state.executionRouting.fingerprint}`);
962
+ }
963
+ else if (state.launchProfile) {
794
964
  lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
795
965
  }
796
966
  if (state.lastError) {
@@ -816,7 +986,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
816
986
  printPanel("Auto-Golang Reset", removed ? `State file ${flowStateFile(config.scope.scopeKey, "auto-golang")} removed.` : "No flow state file found.", "yellow");
817
987
  return false;
818
988
  }
819
- checkPrerequisites(config);
989
+ checkPrerequisites(config, launchProfile, executionRouting);
820
990
  if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
821
991
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
822
992
  process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
@@ -828,22 +998,37 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
828
998
  delete process.env.JIRA_TASK_FILE;
829
999
  }
830
1000
  if (config.command === "plan") {
831
- requireJiraConfig(config);
832
- if (config.verbose) {
833
- process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
834
- process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
835
- process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
1001
+ let taskContextIteration;
1002
+ if (config.jiraRef) {
1003
+ requireJiraConfig(config);
1004
+ if (config.verbose) {
1005
+ process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
1006
+ process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
1007
+ process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
1008
+ }
1009
+ taskContextIteration = nextArtifactIteration(config.taskKey, "task-context", "json");
1010
+ await runDeclarativeFlowBySpecFile("task-source/jira-fetch.json", config, {
1011
+ jiraApiUrl: config.jiraApiUrl,
1012
+ taskKey: config.taskKey,
1013
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1014
+ await runDeclarativeFlowBySpecFile("normalize-task-source.json", config, {
1015
+ taskKey: config.taskKey,
1016
+ iteration: taskContextIteration,
1017
+ extraPrompt: config.extraPrompt,
1018
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1019
+ }
1020
+ else {
1021
+ taskContextIteration = latestTaskContextIteration(config.taskKey);
836
1022
  }
837
1023
  await runDeclarativeFlowBySpecFile("plan.json", config, {
838
- jiraApiUrl: config.jiraApiUrl,
839
1024
  taskKey: config.taskKey,
840
- taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
1025
+ taskContextIteration,
841
1026
  designIteration: nextArtifactIteration(config.taskKey, "design"),
842
1027
  planIteration: nextArtifactIteration(config.taskKey, "plan"),
843
1028
  qaIteration: nextArtifactIteration(config.taskKey, "qa"),
844
1029
  extraPrompt: config.extraPrompt,
845
1030
  forceRefresh: forceRefreshSummary,
846
- }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
1031
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
847
1032
  return false;
848
1033
  }
849
1034
  if (config.command === "bug-analyze") {
@@ -862,7 +1047,103 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
862
1047
  bugFixPlanIteration: nextArtifactIteration(config.taskKey, "bug-fix-plan"),
863
1048
  extraPrompt: config.extraPrompt,
864
1049
  forceRefresh: forceRefreshSummary,
865
- }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
1050
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1051
+ return false;
1052
+ }
1053
+ if (config.command === "design-review") {
1054
+ const iteration = nextDesignReviewIterationForTask(config.taskKey);
1055
+ const inputContract = resolveDesignReviewInputContract(config.taskKey);
1056
+ if (!config.dryRun) {
1057
+ clearReadyToMergeFile(config.taskKey);
1058
+ }
1059
+ await runDeclarativeFlowBySpecFile("design-review.json", config, {
1060
+ taskKey: config.taskKey,
1061
+ iteration,
1062
+ planningIteration: inputContract.planningIteration,
1063
+ designFile: inputContract.designFile,
1064
+ designJsonFile: inputContract.designJsonFile,
1065
+ planFile: inputContract.planFile,
1066
+ planJsonFile: inputContract.planJsonFile,
1067
+ hasQaArtifacts: inputContract.hasQaArtifacts,
1068
+ qaFilePath: inputContract.qaFilePath,
1069
+ qaJsonFilePath: inputContract.qaJsonFilePath,
1070
+ qaFile: inputContract.qaFile,
1071
+ qaJsonFile: inputContract.qaJsonFile,
1072
+ hasTaskContextJsonFile: inputContract.hasTaskContextJsonFile,
1073
+ taskContextJsonFilePath: inputContract.taskContextJsonFilePath,
1074
+ taskContextJsonFile: inputContract.taskContextJsonFile,
1075
+ hasJiraTaskFile: inputContract.hasJiraTaskFile,
1076
+ jiraTaskFilePath: inputContract.jiraTaskFilePath,
1077
+ jiraTaskFile: inputContract.jiraTaskFile,
1078
+ hasJiraAttachmentsManifestFile: inputContract.hasJiraAttachmentsManifestFile,
1079
+ jiraAttachmentsManifestFilePath: inputContract.jiraAttachmentsManifestFilePath,
1080
+ jiraAttachmentsManifestFile: inputContract.jiraAttachmentsManifestFile,
1081
+ hasJiraAttachmentsContextFile: inputContract.hasJiraAttachmentsContextFile,
1082
+ jiraAttachmentsContextFilePath: inputContract.jiraAttachmentsContextFilePath,
1083
+ jiraAttachmentsContextFile: inputContract.jiraAttachmentsContextFile,
1084
+ hasPlanningAnswersJsonFile: inputContract.hasPlanningAnswersJsonFile,
1085
+ planningAnswersJsonFilePath: inputContract.planningAnswersJsonFilePath,
1086
+ planningAnswersJsonFile: inputContract.planningAnswersJsonFile,
1087
+ hasTaskInputJsonFile: inputContract.hasTaskInputJsonFile,
1088
+ taskInputJsonFilePath: inputContract.taskInputJsonFilePath,
1089
+ taskInputJsonFile: inputContract.taskInputJsonFile,
1090
+ extraPrompt: config.extraPrompt,
1091
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1092
+ if (!config.dryRun) {
1093
+ printSummary("Design Review", `Artifacts:\n${designReviewFile(config.taskKey, iteration)}\n${designReviewJsonFile(config.taskKey, iteration)}`);
1094
+ }
1095
+ return false;
1096
+ }
1097
+ if (config.command === "plan-revise") {
1098
+ const inputContract = resolvePlanReviseInputContract(config.taskKey);
1099
+ if (!config.dryRun) {
1100
+ clearReadyToMergeFile(config.taskKey);
1101
+ }
1102
+ await runDeclarativeFlowBySpecFile("plan-revise.json", config, {
1103
+ taskKey: config.taskKey,
1104
+ reviewIteration: inputContract.reviewIteration,
1105
+ reviewFile: inputContract.reviewFile,
1106
+ reviewJsonFile: inputContract.reviewJsonFile,
1107
+ sourcePlanningIteration: inputContract.sourcePlanningIteration,
1108
+ outputIteration: inputContract.outputIteration,
1109
+ designFile: inputContract.designFile,
1110
+ designJsonFile: inputContract.designJsonFile,
1111
+ planFile: inputContract.planFile,
1112
+ planJsonFile: inputContract.planJsonFile,
1113
+ hasQaArtifacts: inputContract.hasQaArtifacts,
1114
+ qaFilePath: inputContract.qaFilePath,
1115
+ qaJsonFilePath: inputContract.qaJsonFilePath,
1116
+ qaFile: inputContract.qaFile,
1117
+ qaJsonFile: inputContract.qaJsonFile,
1118
+ revisedDesignFile: inputContract.revisedDesignFile,
1119
+ revisedDesignJsonFile: inputContract.revisedDesignJsonFile,
1120
+ revisedPlanFile: inputContract.revisedPlanFile,
1121
+ revisedPlanJsonFile: inputContract.revisedPlanJsonFile,
1122
+ revisedQaFile: inputContract.revisedQaFile,
1123
+ revisedQaJsonFile: inputContract.revisedQaJsonFile,
1124
+ hasTaskContextJsonFile: inputContract.hasTaskContextJsonFile,
1125
+ taskContextJsonFilePath: inputContract.taskContextJsonFilePath,
1126
+ taskContextJsonFile: inputContract.taskContextJsonFile,
1127
+ hasJiraTaskFile: inputContract.hasJiraTaskFile,
1128
+ jiraTaskFilePath: inputContract.jiraTaskFilePath,
1129
+ jiraTaskFile: inputContract.jiraTaskFile,
1130
+ hasJiraAttachmentsManifestFile: inputContract.hasJiraAttachmentsManifestFile,
1131
+ jiraAttachmentsManifestFilePath: inputContract.jiraAttachmentsManifestFilePath,
1132
+ jiraAttachmentsManifestFile: inputContract.jiraAttachmentsManifestFile,
1133
+ hasJiraAttachmentsContextFile: inputContract.hasJiraAttachmentsContextFile,
1134
+ jiraAttachmentsContextFilePath: inputContract.jiraAttachmentsContextFilePath,
1135
+ jiraAttachmentsContextFile: inputContract.jiraAttachmentsContextFile,
1136
+ hasPlanningAnswersJsonFile: inputContract.hasPlanningAnswersJsonFile,
1137
+ planningAnswersJsonFilePath: inputContract.planningAnswersJsonFilePath,
1138
+ planningAnswersJsonFile: inputContract.planningAnswersJsonFile,
1139
+ hasTaskInputJsonFile: inputContract.hasTaskInputJsonFile,
1140
+ taskInputJsonFilePath: inputContract.taskInputJsonFilePath,
1141
+ taskInputJsonFile: inputContract.taskInputJsonFile,
1142
+ extraPrompt: config.extraPrompt,
1143
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1144
+ if (!config.dryRun) {
1145
+ printSummary("Plan Revise", `Artifacts:\n${inputContract.revisedDesignFile}\n${inputContract.revisedDesignJsonFile}\n${inputContract.revisedPlanFile}\n${inputContract.revisedPlanJsonFile}\n${inputContract.revisedQaFile}\n${inputContract.revisedQaJsonFile}`);
1146
+ }
866
1147
  return false;
867
1148
  }
868
1149
  if (config.command === "gitlab-review") {
@@ -875,7 +1156,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
875
1156
  extraPrompt: config.extraPrompt,
876
1157
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, iteration),
877
1158
  reviewFixPoints: config.reviewFixPoints,
878
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1159
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
879
1160
  if (!config.dryRun) {
880
1161
  printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}\n${reviewAssessmentFile(config.taskKey, iteration)}\n${reviewAssessmentJsonFile(config.taskKey, iteration)}`);
881
1162
  }
@@ -889,7 +1170,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
889
1170
  iteration,
890
1171
  gitlabDiffIteration,
891
1172
  extraPrompt: config.extraPrompt,
892
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1173
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
893
1174
  if (!config.dryRun) {
894
1175
  printSummary("GitLab Diff Review", `Artifacts:\n${gitlabDiffFile(config.taskKey)}\n${gitlabDiffJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}`);
895
1176
  }
@@ -907,7 +1188,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
907
1188
  await runDeclarativeFlowBySpecFile("bugz/bug-fix.json", config, {
908
1189
  taskKey: config.taskKey,
909
1190
  extraPrompt: config.extraPrompt,
910
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1191
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
911
1192
  return false;
912
1193
  }
913
1194
  if (config.command === "mr-description") {
@@ -917,7 +1198,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
917
1198
  taskKey: config.taskKey,
918
1199
  iteration: nextArtifactIteration(config.taskKey, "mr-description"),
919
1200
  extraPrompt: config.extraPrompt,
920
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1201
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
921
1202
  return false;
922
1203
  }
923
1204
  if (config.command === "task-describe") {
@@ -927,42 +1208,33 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
927
1208
  taskKey: config.taskKey,
928
1209
  iteration,
929
1210
  extraPrompt: config.extraPrompt,
930
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1211
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
931
1212
  return false;
932
1213
  }
933
1214
  if (config.command === "implement") {
934
- requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
935
- validateStructuredArtifacts([
936
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
937
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
938
- { path: qaJsonFile(config.taskKey), schemaId: "qa-plan/v1" },
939
- ], "Implement mode requires valid structured plan artifacts from the planning phase.");
1215
+ const planningBundle = resolveLatestPlanningBundle(config.taskKey);
940
1216
  await runDeclarativeFlowBySpecFile("implement.json", config, {
941
1217
  taskKey: config.taskKey,
1218
+ planningIteration: planningBundle.planningIteration,
942
1219
  extraPrompt: config.extraPrompt,
943
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode);
1220
+ }, flowOverrides, requestUserInput, undefined, launchMode);
944
1221
  return false;
945
1222
  }
946
1223
  if (config.command === "review") {
947
1224
  const iteration = nextReviewIterationForTask(config.taskKey);
948
- if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
949
- requireJiraConfig(config);
950
- validateStructuredArtifacts([
951
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
952
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
953
- ], "Review mode requires valid structured plan artifacts from the planning phase.");
1225
+ if (hasStructuredReviewInputs(config.taskKey)) {
954
1226
  await runDeclarativeFlowBySpecFile("review/review.json", config, {
955
- taskKey: config.taskKey,
1227
+ ...reviewFlowParamsFromContract(config),
956
1228
  iteration,
957
1229
  extraPrompt: config.extraPrompt,
958
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1230
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
959
1231
  }
960
1232
  else {
961
1233
  await runDeclarativeFlowBySpecFile("review/review-project.json", config, {
962
1234
  taskKey: config.taskKey,
963
1235
  iteration,
964
1236
  extraPrompt: config.extraPrompt,
965
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1237
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
966
1238
  }
967
1239
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
968
1240
  }
@@ -981,23 +1253,21 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
981
1253
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
982
1254
  extraPrompt: config.extraPrompt,
983
1255
  reviewFixPoints: config.reviewFixPoints,
984
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1256
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
985
1257
  return false;
986
1258
  }
987
1259
  if (config.command === "review-loop") {
988
1260
  const iteration = nextReviewIterationForTask(config.taskKey);
989
- if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
990
- requireJiraConfig(config);
991
- validateStructuredArtifacts([
992
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
993
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
994
- ], "Review-loop mode requires valid structured plan artifacts from the planning phase.");
995
- }
996
- await runDeclarativeFlowBySpecFile("review/review-loop.json", config, {
997
- taskKey: config.taskKey,
998
- iteration,
1261
+ const reviewLoopSpec = hasStructuredReviewInputs(config.taskKey)
1262
+ ? "review/review-loop.json"
1263
+ : "review/review-project-loop.json";
1264
+ await runDeclarativeFlowBySpecFile(reviewLoopSpec, config, {
1265
+ ...(reviewLoopSpec === "review/review-loop.json"
1266
+ ? reviewFlowParamsFromContract(config)
1267
+ : { taskKey: config.taskKey }),
1268
+ baseIteration: iteration,
999
1269
  extraPrompt: config.extraPrompt,
1000
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1270
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1001
1271
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
1002
1272
  }
1003
1273
  if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
@@ -1008,14 +1278,14 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1008
1278
  runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
1009
1279
  runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
1010
1280
  extraPrompt: config.extraPrompt,
1011
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1281
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1012
1282
  return false;
1013
1283
  }
1014
1284
  if (config.command === "git-commit") {
1015
1285
  await runDeclarativeFlowBySpecFile("git-commit.json", config, {
1016
1286
  taskKey: config.taskKey,
1017
1287
  extraPrompt: config.extraPrompt,
1018
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1288
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1019
1289
  return false;
1020
1290
  }
1021
1291
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
@@ -1043,6 +1313,7 @@ function parseCliArgs(argv) {
1043
1313
  let prompt;
1044
1314
  let autoFromPhase;
1045
1315
  let scopeName;
1316
+ let reviewBlockingSeverities;
1046
1317
  let helpPhases = false;
1047
1318
  let jiraRef;
1048
1319
  let mdLang;
@@ -1071,6 +1342,15 @@ function parseCliArgs(argv) {
1071
1342
  index += 1;
1072
1343
  continue;
1073
1344
  }
1345
+ if (token === "--blocking-severities") {
1346
+ reviewBlockingSeverities = parseReviewSeverityCsv(argv[index + 1] ?? "");
1347
+ index += 1;
1348
+ continue;
1349
+ }
1350
+ if (token.startsWith("--blocking-severities=")) {
1351
+ reviewBlockingSeverities = parseReviewSeverityCsv(token.slice("--blocking-severities=".length));
1352
+ continue;
1353
+ }
1074
1354
  if (token === "--from") {
1075
1355
  autoFromPhase = argv[index + 1];
1076
1356
  index += 1;
@@ -1114,6 +1394,10 @@ function parseCliArgs(argv) {
1114
1394
  printAutoCommonPhasesHelp();
1115
1395
  process.exit(0);
1116
1396
  }
1397
+ if (command === "auto-simple" && helpPhases) {
1398
+ printAutoSimplePhasesHelp();
1399
+ process.exit(0);
1400
+ }
1117
1401
  return {
1118
1402
  command: command,
1119
1403
  dry,
@@ -1121,6 +1405,7 @@ function parseCliArgs(argv) {
1121
1405
  helpPhases,
1122
1406
  ...(jiraRef !== undefined ? { jiraRef } : {}),
1123
1407
  ...(scopeName !== undefined ? { scopeName } : {}),
1408
+ ...(reviewBlockingSeverities !== undefined ? { reviewBlockingSeverities } : {}),
1124
1409
  ...(prompt !== undefined ? { prompt } : {}),
1125
1410
  ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
1126
1411
  ...(mdLang !== undefined ? { mdLang } : {}),
@@ -1131,6 +1416,7 @@ function buildConfigFromArgs(args) {
1131
1416
  return buildBaseConfig(args.command, {
1132
1417
  ...(args.jiraRef !== undefined ? { jiraRef: args.jiraRef } : {}),
1133
1418
  ...(args.scopeName !== undefined ? { scopeName: args.scopeName } : {}),
1419
+ ...(args.reviewBlockingSeverities !== undefined ? { reviewBlockingSeverities: args.reviewBlockingSeverities } : {}),
1134
1420
  ...(args.prompt !== undefined ? { extraPrompt: args.prompt } : {}),
1135
1421
  ...(args.autoFromPhase !== undefined ? { autoFromPhase: args.autoFromPhase } : {}),
1136
1422
  ...(args.mdLang !== undefined ? { mdLang: args.mdLang } : {}),
@@ -1146,7 +1432,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1146
1432
  let activeAbortController = null;
1147
1433
  let activeFlowId = null;
1148
1434
  let exiting = false;
1149
- const ui = new InteractiveUi({
1435
+ const ui = createInteractiveSession({
1150
1436
  scopeKey: currentScope.scopeKey,
1151
1437
  jiraIssueKey: currentScope.jiraIssueKey ?? null,
1152
1438
  summaryText: "",
@@ -1171,40 +1457,51 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1171
1457
  if (!flowEntry) {
1172
1458
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1173
1459
  }
1460
+ const routingGroups = flowRoutingGroups(flowEntry, process.cwd());
1174
1461
  const resumeState = launchMode === "resume" ? loadFlowRunState(currentScope.scopeKey, flowId) : null;
1175
1462
  if (resumeState) {
1176
1463
  currentScope = scopeWithRestoredJiraContext(currentScope, resumeState);
1177
1464
  }
1178
- const launchProfile = launchMode === "resume"
1179
- ? resumeState?.launchProfile
1180
- : await requestInteractiveLaunchProfile((form) => ui.requestUserInput(form));
1181
- if (!launchProfile) {
1182
- throw new TaskRunnerError("Resume is impossible because launch profile was not saved. Use restart.");
1465
+ const routingSelection = launchMode === "resume"
1466
+ ? (resumeState?.executionRouting
1467
+ ? {
1468
+ routing: resumeState.executionRouting,
1469
+ selectedPreset: resumeState.selectedRoutingPreset ?? { kind: "custom", label: "Saved routing" },
1470
+ }
1471
+ : null)
1472
+ : await requestInteractiveExecutionRouting(flowEntry, (form) => ui.requestUserInput(form));
1473
+ if (launchMode === "resume" && !routingSelection?.routing) {
1474
+ throw new TaskRunnerError("Resume is impossible because execution routing was not saved. Use restart.");
1183
1475
  }
1476
+ const launchProfile = routingSelection?.routing?.defaultRoute;
1184
1477
  const previousScopeKey = currentScope.scopeKey;
1185
- const baseConfig = buildBaseConfig(flowId, {
1186
- ...(currentScope.jiraRef ? { jiraRef: currentScope.jiraRef } : {}),
1187
- scopeName: currentScope.scopeKey,
1188
- });
1478
+ const baseConfig = buildInteractiveBaseConfig(flowId, currentScope);
1189
1479
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1190
1480
  const nextScope = await resolveScopeForCommand(baseConfig, (form) => ui.requestUserInput(form));
1191
1481
  currentScope = nextScope;
1192
1482
  }
1193
1483
  else if (flowRequiresTaskScope(flowEntry) && !currentScope.jiraRef) {
1194
1484
  const jiraContext = await requestJiraContext((form) => ui.requestUserInput(form));
1195
- currentScope = attachJiraContext(currentScope, jiraContext.jiraRef);
1485
+ currentScope = resolveProjectScope(null, jiraContext.jiraRef);
1196
1486
  }
1197
1487
  ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null);
1198
1488
  if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
1199
1489
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1200
1490
  }
1201
- printPanel("Effective Launch Config", `executor: ${launchProfile.executor}\nmodel: ${launchProfile.model}\nmode: ${launchMode}`, "cyan");
1491
+ if (routingSelection?.routing) {
1492
+ printPanel("Effective Launch Config", `preset: ${routingSelection.selectedPreset.label}\nmode: ${launchMode}\n${describeExecutionRouting(routingSelection.routing, routingGroups)}`, "cyan");
1493
+ }
1202
1494
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1203
- await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, launchProfile, createRuntimeServices(abortController.signal));
1495
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, launchProfile, routingSelection?.routing, routingSelection?.selectedPreset, createRuntimeServices(abortController.signal));
1204
1496
  return;
1205
1497
  }
1206
1498
  const runtimeConfig = buildRuntimeConfig(baseConfig, currentScope);
1207
- await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh, { launchProfile }), { launchProfile }, (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1499
+ const flowOverrides = {
1500
+ ...(launchProfile ? { launchProfile } : {}),
1501
+ ...(routingSelection?.routing ? { executionRouting: routingSelection.routing } : {}),
1502
+ ...(routingSelection?.selectedPreset ? { selectedRoutingPreset: routingSelection.selectedPreset } : {}),
1503
+ };
1504
+ await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh, flowOverrides), flowOverrides, (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1208
1505
  }
1209
1506
  catch (error) {
1210
1507
  if (error instanceof FlowInterruptedError) {
@@ -1283,7 +1580,10 @@ export async function main(argv = process.argv.slice(2)) {
1283
1580
  return await runInteractive(args[0] ?? "", forceRefresh);
1284
1581
  }
1285
1582
  const parsedArgs = parseCliArgs(args);
1286
- await executeCommand(buildConfigFromArgs(parsedArgs));
1583
+ const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs));
1584
+ if (parsedArgs.command === "doctor") {
1585
+ return commandCompleted ? 0 : 1;
1586
+ }
1287
1587
  return 0;
1288
1588
  }
1289
1589
  catch (error) {