agentweaver 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +76 -19
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +88 -3
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/executors.js +2 -2
  6. package/dist/doctor/checks/flow-readiness.js +15 -18
  7. package/dist/flow-state.js +212 -15
  8. package/dist/index.js +539 -209
  9. package/dist/interactive/blessed-session.js +361 -0
  10. package/dist/interactive/controller.js +1326 -0
  11. package/dist/interactive/create-interactive-session.js +5 -0
  12. package/dist/interactive/ink/index.js +597 -0
  13. package/dist/interactive/progress.js +245 -0
  14. package/dist/interactive/selectors.js +14 -0
  15. package/dist/interactive/session.js +1 -0
  16. package/dist/interactive/state.js +34 -0
  17. package/dist/interactive/tree.js +155 -0
  18. package/dist/interactive/types.js +1 -0
  19. package/dist/interactive/view-model.js +1 -0
  20. package/dist/interactive-ui.js +159 -194
  21. package/dist/pipeline/auto-flow.js +9 -6
  22. package/dist/pipeline/context.js +7 -5
  23. package/dist/pipeline/declarative-flow-runner.js +212 -6
  24. package/dist/pipeline/declarative-flows.js +63 -17
  25. package/dist/pipeline/execution-routing-config.js +15 -0
  26. package/dist/pipeline/flow-catalog.js +50 -12
  27. package/dist/pipeline/flow-run-resume.js +29 -0
  28. package/dist/pipeline/flow-specs/auto-common.json +90 -360
  29. package/dist/pipeline/flow-specs/auto-golang.json +81 -360
  30. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  31. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  32. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  33. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +316 -0
  34. package/dist/pipeline/flow-specs/design-review.json +10 -0
  35. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  36. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  37. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  38. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  39. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  40. package/dist/pipeline/flow-specs/implement.json +13 -6
  41. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  42. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  43. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  44. package/dist/pipeline/flow-specs/plan.json +51 -71
  45. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  46. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  47. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  48. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  49. package/dist/pipeline/flow-specs/review/review.json +37 -31
  50. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  51. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  52. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  53. package/dist/pipeline/launch-profile-config.js +30 -18
  54. package/dist/pipeline/node-contract.js +1 -0
  55. package/dist/pipeline/node-registry.js +115 -6
  56. package/dist/pipeline/node-runner.js +3 -2
  57. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  58. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  59. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  60. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  61. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  62. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  63. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  64. package/dist/pipeline/nodes/flow-run-node.js +242 -8
  65. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  66. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  67. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  68. package/dist/pipeline/nodes/llm-prompt-node.js +38 -36
  69. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  70. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  71. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  72. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  73. package/dist/pipeline/nodes/user-input-node.js +38 -3
  74. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  75. package/dist/pipeline/plugin-loader.js +389 -0
  76. package/dist/pipeline/plugin-types.js +1 -0
  77. package/dist/pipeline/prompt-registry.js +3 -1
  78. package/dist/pipeline/prompt-runtime.js +4 -1
  79. package/dist/pipeline/registry.js +71 -4
  80. package/dist/pipeline/review-iteration.js +26 -0
  81. package/dist/pipeline/spec-compiler.js +3 -0
  82. package/dist/pipeline/spec-loader.js +14 -0
  83. package/dist/pipeline/spec-types.js +3 -0
  84. package/dist/pipeline/spec-validator.js +20 -0
  85. package/dist/pipeline/value-resolver.js +76 -2
  86. package/dist/plugin-sdk.js +1 -0
  87. package/dist/prompts.js +36 -14
  88. package/dist/review-severity.js +45 -0
  89. package/dist/runtime/artifact-registry.js +405 -0
  90. package/dist/runtime/design-review-input-contract.js +17 -16
  91. package/dist/runtime/env-loader.js +3 -0
  92. package/dist/runtime/execution-routing-store.js +134 -0
  93. package/dist/runtime/execution-routing.js +233 -0
  94. package/dist/runtime/interactive-execution-routing.js +471 -0
  95. package/dist/runtime/plan-revise-input-contract.js +35 -32
  96. package/dist/runtime/planning-bundle.js +123 -0
  97. package/dist/runtime/ready-to-merge.js +22 -1
  98. package/dist/runtime/review-input-contract.js +100 -0
  99. package/dist/structured-artifact-schema-registry.js +9 -0
  100. package/dist/structured-artifact-schemas.json +140 -1
  101. package/dist/structured-artifacts.js +77 -6
  102. package/dist/user-input.js +70 -3
  103. package/docs/example/.flows/examples/claude-example.json +50 -0
  104. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  105. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  106. package/docs/examples/.flows/claude-example.json +50 -0
  107. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  108. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  109. package/docs/plugin-sdk.md +731 -0
  110. package/package.json +11 -4
package/dist/index.js CHANGED
@@ -3,27 +3,35 @@ 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 { bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designReviewFile, designReviewJsonFile, 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 { archiveActiveAttempt, 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
- import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, rewindFlowRunStateToPhase, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
8
+ import { createFlowRunState, classifyFlowLaunchAvailability, loadFlowRunState, prepareFlowStateForContinue, 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 { createPipelineRegistryContext } from "./pipeline/plugin-loader.js";
19
+ import { DEFAULT_LAUNCH_PROFILE, } from "./pipeline/launch-profile-config.js";
20
+ import { withCanonicalReviewLoopParams } from "./pipeline/review-iteration.js";
18
21
  import { evaluateCondition, resolveValue } from "./pipeline/value-resolver.js";
19
22
  import { resolveCmd } from "./runtime/command-resolution.js";
20
23
  import { loadTieredEnv } from "./runtime/env-loader.js";
21
24
  import { agentweaverHome } from "./runtime/agentweaver-home.js";
22
25
  import { runCommand } from "./runtime/process-runner.js";
26
+ import { createArtifactRegistry } from "./runtime/artifact-registry.js";
23
27
  import { resolveDesignReviewInputContract } from "./runtime/design-review-input-contract.js";
24
28
  import { resolvePlanReviseInputContract } from "./runtime/plan-revise-input-contract.js";
29
+ import { resolveLatestPlanningBundle } from "./runtime/planning-bundle.js";
30
+ import { inspectReviewInputContract, resolveReviewInputContract } from "./runtime/review-input-contract.js";
25
31
  import { clearReadyToMergeFile } from "./runtime/ready-to-merge.js";
26
- import { InteractiveUi } from "./interactive-ui.js";
32
+ import { describeExecutionRouting, executorsForRoutingGroups, resolveExecutionRouting, } from "./runtime/execution-routing.js";
33
+ import { requestInteractiveExecutionRouting } from "./runtime/interactive-execution-routing.js";
34
+ import { createInteractiveSession } from "./interactive/create-interactive-session.js";
27
35
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
28
36
  import { requestUserInputInTerminal } from "./user-input.js";
29
37
  import { runDoctorCommand } from "./doctor/index.js";
@@ -31,6 +39,7 @@ import { detectGitBranchName, requestJiraContext, resolveProjectScope, } from ".
31
39
  const COMMANDS = [
32
40
  "auto-golang",
33
41
  "auto-common",
42
+ "auto-simple",
34
43
  "auto-status",
35
44
  "auto-reset",
36
45
  "bug-analyze",
@@ -40,6 +49,7 @@ const COMMANDS = [
40
49
  "git-commit",
41
50
  "gitlab-diff-review",
42
51
  "gitlab-review",
52
+ "instant-task",
43
53
  "mr-description",
44
54
  "plan",
45
55
  "plan-revise",
@@ -57,6 +67,7 @@ function createRuntimeServices(signal) {
57
67
  return {
58
68
  resolveCmd,
59
69
  runCommand: (argv, options = {}) => runCommand(argv, { ...options, ...(signal ? { signal } : {}) }),
70
+ artifactRegistry: createArtifactRegistry(),
60
71
  };
61
72
  }
62
73
  const runtimeServices = createRuntimeServices();
@@ -106,19 +117,24 @@ function usage() {
106
117
  agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
107
118
  agentweaver design-review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
108
119
  agentweaver doctor [<category>|<check-id>] [--json]
120
+ agentweaver instant-task [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>]
109
121
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
110
122
  agentweaver plan [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [<jira-browse-url|jira-issue-key>]
111
123
  agentweaver plan-revise [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
112
124
  agentweaver task-describe [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
113
125
  agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
114
- agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
115
- agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
116
- agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
126
+ agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
127
+ agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
128
+ agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
117
129
  agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
118
130
  agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
119
131
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
120
132
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
121
133
  agentweaver auto-golang --help-phases
134
+ agentweaver auto-common [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
135
+ agentweaver auto-common --help-phases
136
+ agentweaver auto-simple [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
137
+ agentweaver auto-simple --help-phases
122
138
  agentweaver auto-status [<jira-browse-url|jira-issue-key>]
123
139
  agentweaver auto-reset [<jira-browse-url|jira-issue-key>]
124
140
 
@@ -132,8 +148,12 @@ Flags:
132
148
  --force In interactive mode, regenerate task summary in Jira-backed flows
133
149
  --dry Fetch Jira task, but print codex/opencode commands instead of executing them
134
150
  --verbose Show live stdout/stderr of launched commands
135
- --scope Explicit workflow scope name for non-Jira runs
151
+ --scope Explicit workflow scope name for non-Jira runs except instant-task
136
152
  --prompt Extra prompt text appended to the base prompt
153
+ --resume Resume an interrupted run when valid
154
+ --continue Continue a terminated iterative run when valid
155
+ --restart Archive the active attempt and start a fresh run
156
+ --blocking-severities Comma-separated severities that block merge and drive review-fix auto-selection
137
157
  --md-lang Language for markdown output files: en (English) or ru (Russian, default)
138
158
 
139
159
  Required environment variables:
@@ -145,6 +165,7 @@ Optional environment variables:
145
165
  JIRA_BASE_URL
146
166
  GITLAB_TOKEN
147
167
  AGENTWEAVER_HOME
168
+ ${AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV}
148
169
  CODEX_BIN
149
170
  CODEX_MODEL
150
171
  OPENCODE_BIN
@@ -152,8 +173,11 @@ Optional environment variables:
152
173
 
153
174
  Notes:
154
175
  - 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.
176
+ - instant-task always uses the current branch-derived project scope and rejects explicit scope overrides or Jira arguments.
155
177
  - All flow state and artifacts are stored in the current project scope by default.
156
- - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.`;
178
+ - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.
179
+ - ${AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV} sets the default blocking severities. Default: blocker,critical,high.
180
+ - Interactive mode requires Ink runtime dependencies and a real TTY.`;
157
181
  }
158
182
  function packageVersion() {
159
183
  const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
@@ -166,12 +190,12 @@ function packageVersion() {
166
190
  function normalizeAutoPhaseId(phaseId) {
167
191
  return phaseId.trim().toLowerCase().replaceAll("-", "_");
168
192
  }
169
- function autoPhaseIds() {
170
- return loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases.map((phase) => phase.id);
193
+ async function autoPhaseIds() {
194
+ return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" })).phases.map((phase) => phase.id);
171
195
  }
172
- function validateAutoPhaseId(phaseId) {
196
+ async function validateAutoPhaseId(phaseId) {
173
197
  const normalized = normalizeAutoPhaseId(phaseId);
174
- if (!autoPhaseIds().includes(normalized)) {
198
+ if (!(await autoPhaseIds()).includes(normalized)) {
175
199
  throw new TaskRunnerError(`Unknown auto-golang phase: ${phaseId}\nUse 'agentweaver auto-golang --help-phases' or '/help auto-golang' to list valid phases.`);
176
200
  }
177
201
  return normalized;
@@ -183,7 +207,11 @@ function buildFlowResumeDetails(state) {
183
207
  `Current step: ${currentStep}`,
184
208
  `Updated: ${state.updatedAt}`,
185
209
  ];
186
- if (state.launchProfile) {
210
+ if (state.executionRouting) {
211
+ lines.push(`Default route: ${state.executionRouting.defaultRoute.executor} / ${state.executionRouting.defaultRoute.model}`);
212
+ lines.push(`Routing fingerprint: ${state.executionRouting.fingerprint}`);
213
+ }
214
+ else if (state.launchProfile) {
187
215
  lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
188
216
  }
189
217
  if (state.lastError) {
@@ -191,78 +219,16 @@ function buildFlowResumeDetails(state) {
191
219
  }
192
220
  return lines.join("\n");
193
221
  }
194
- function launchProfileSelectionForm() {
195
- const defaultExecutor = DEFAULT_LAUNCH_PROFILE.executor;
196
- return {
197
- formId: "flow-launch-profile",
198
- title: "LLM Launch Settings",
199
- description: `Select an executor for the flow. Current default: ${defaultExecutor}.`,
200
- submitLabel: "Continue",
201
- fields: [
202
- {
203
- id: "executor",
204
- type: "single-select",
205
- label: "Executor",
206
- required: true,
207
- default: defaultExecutor,
208
- options: LLM_EXECUTOR_IDS.map((id) => ({
209
- value: id,
210
- label: id === defaultExecutor ? `${id} [default]` : id,
211
- })),
212
- },
213
- ],
214
- };
215
- }
216
- function launchModelSelectionForm(executor) {
217
- const resolvedExecutor = executor === "default" ? DEFAULT_LAUNCH_PROFILE.executor : executor;
218
- const defaultModel = defaultModelForExecutor(resolvedExecutor);
219
- const options = ALLOWED_MODELS_BY_EXECUTOR[resolvedExecutor].map((model) => ({
220
- value: model,
221
- label: model === defaultModel ? `${model} [default]` : model,
222
- }));
223
- return {
224
- formId: "flow-launch-model",
225
- title: "LLM Launch Settings",
226
- description: `Select a model for the flow. Current default for ${resolvedExecutor}: ${defaultModel}.`,
227
- submitLabel: "Start",
228
- fields: [
229
- {
230
- id: "model",
231
- type: "single-select",
232
- label: "Model",
233
- required: true,
234
- default: defaultModel,
235
- options,
236
- },
237
- ],
238
- };
239
- }
240
- function isFormCancellation(error, formId) {
241
- return error instanceof TaskRunnerError && error.message === `User cancelled form '${formId}'.`;
242
- }
243
- async function requestInteractiveLaunchProfile(requestUserInput) {
244
- for (;;) {
245
- const executorFormResult = await requestUserInput(launchProfileSelectionForm());
246
- const rawExecutor = String(executorFormResult.values.executor ?? DEFAULT_LAUNCH_PROFILE.executor);
247
- const executor = LLM_EXECUTOR_IDS.find((id) => id === rawExecutor);
248
- if (!executor) {
249
- throw new TaskRunnerError(`Unsupported launch executor '${rawExecutor}'.`);
250
- }
251
- try {
252
- const modelFormResult = await requestUserInput(launchModelSelectionForm(executor));
253
- const rawModel = String(modelFormResult.values.model ?? defaultModelForExecutor(executor)).trim();
254
- return resolveLaunchProfile({
255
- executor,
256
- model: rawModel.length > 0 ? rawModel : defaultModelForExecutor(executor),
257
- }, DEFAULT_LAUNCH_PROFILE);
258
- }
259
- catch (error) {
260
- if (isFormCancellation(error, "flow-launch-model")) {
261
- continue;
262
- }
263
- throw error;
264
- }
222
+ function buildFlowContinueDetails(state) {
223
+ const lines = [
224
+ "Continuable loop boundary found.",
225
+ `Updated: ${state.updatedAt}`,
226
+ ];
227
+ if (state.continuation?.stopPhaseId && state.continuation?.stopStepId) {
228
+ lines.push(`Stopped at: ${state.continuation.stopPhaseId}:${state.continuation.stopStepId}`);
265
229
  }
230
+ lines.push("Continue will preserve existing artifacts and start the next iteration from active inputs.");
231
+ return lines.join("\n");
266
232
  }
267
233
  function buildResolverContext(pipelineContext, flowParams, flowConstants, repeatVars, executionState) {
268
234
  return {
@@ -337,19 +303,27 @@ function validateDeclarativePhaseResumeState(phase, phaseState, pipelineContext,
337
303
  }
338
304
  }
339
305
  }
340
- function validateDeclarativeFlowResumeState(flowEntry, config, state, launchProfile, runtime = runtimeServices) {
341
- if (state.launchProfile) {
342
- if (!launchProfile) {
343
- throw new TaskRunnerError("Resume is impossible because launch profile is missing. Use restart.");
306
+ async function validateDeclarativeFlowResumeState(flowEntry, config, state, executionRouting, runtime = runtimeServices) {
307
+ if (state.flowId === "auto-common") {
308
+ const persistedPhaseIds = state.executionState.phases.map((p) => p.id);
309
+ const hasLegacyPlanningGatePhases = persistedPhaseIds.some((id) => ["design_review", "verdict", "plan_revision", "design_review_repeat", "verdict_repeat"].includes(id));
310
+ if (hasLegacyPlanningGatePhases) {
311
+ throw new TaskRunnerError("Resume is impossible because the persisted state was created with the legacy phase graph. Use restart.");
344
312
  }
345
- if (state.launchProfile.fingerprint !== launchProfile.fingerprint) {
346
- throw new TaskRunnerError(`Resume is impossible because launch profile changed (${state.launchProfile.executor}/${state.launchProfile.model} -> ${launchProfile.executor}/${launchProfile.model}). Use restart.`);
313
+ }
314
+ const persistedFingerprint = state.routingFingerprint ?? state.executionRouting?.fingerprint ?? state.launchProfile?.fingerprint;
315
+ if (persistedFingerprint) {
316
+ if (!executionRouting) {
317
+ throw new TaskRunnerError("Resume is impossible because execution routing is missing. Use restart.");
318
+ }
319
+ if (persistedFingerprint !== executionRouting.fingerprint) {
320
+ throw new TaskRunnerError("Resume is impossible because execution routing changed. Use restart.");
347
321
  }
348
322
  }
349
323
  if (flowRequiresTaskScope(flowEntry) && !config.jiraRef) {
350
324
  throw new TaskRunnerError("Resume is impossible because Jira context is missing for this flow state. Use restart.");
351
325
  }
352
- const pipelineContext = createPipelineContext({
326
+ const pipelineContext = await createPipelineContext({
353
327
  issueKey: config.taskKey,
354
328
  jiraRef: config.jiraRef,
355
329
  dryRun: config.dryRun,
@@ -357,8 +331,9 @@ function validateDeclarativeFlowResumeState(flowEntry, config, state, launchProf
357
331
  ...(config.mdLang !== undefined ? { mdLang: config.mdLang } : {}),
358
332
  runtime,
359
333
  requestUserInput: requestUserInputInTerminal,
334
+ ...(executionRouting ? { executionRouting } : {}),
360
335
  });
361
- const flowParams = defaultDeclarativeFlowParams(config, false, launchProfile ? { launchProfile } : {});
336
+ const flowParams = defaultDeclarativeFlowParams(config, false, executionRouting ? { executionRouting, launchProfile: executionRouting.defaultRoute } : {});
362
337
  for (const phase of flowEntry.flow.phases) {
363
338
  const phaseState = state.executionState.phases.find((candidate) => candidate.id === phase.id);
364
339
  if (!phaseState) {
@@ -375,49 +350,65 @@ function scopeWithRestoredJiraContext(scope, state) {
375
350
  }
376
351
  function buildInteractiveBaseConfig(flowId, scope) {
377
352
  return buildBaseConfig(flowId, {
378
- ...(scope.jiraRef ? { jiraRef: scope.jiraRef } : {}),
353
+ ...(flowId !== "instant-task" && scope.jiraRef ? { jiraRef: scope.jiraRef } : {}),
379
354
  });
380
355
  }
381
- function lookupInteractiveFlowResume(flowEntry, currentScope) {
356
+ async function lookupInteractiveFlowResume(flowEntry, currentScope) {
382
357
  const directState = loadFlowRunState(currentScope.scopeKey, flowEntry.id);
383
- if (directState && hasResumableFlowState(directState)) {
358
+ const availability = classifyFlowLaunchAvailability(directState);
359
+ if (directState && availability.resume.available) {
384
360
  try {
385
361
  const effectiveScope = scopeWithRestoredJiraContext(currentScope, directState);
386
362
  const baseConfig = buildInteractiveBaseConfig(flowEntry.id, effectiveScope);
387
363
  const config = buildRuntimeConfig(baseConfig, effectiveScope);
388
- validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.launchProfile);
364
+ await validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.executionRouting);
389
365
  return {
390
- resumeAvailable: true,
391
- hasExistingState: true,
366
+ ...availability,
392
367
  details: buildFlowResumeDetails(directState),
393
368
  };
394
369
  }
395
370
  catch (error) {
396
371
  return {
397
- resumeAvailable: false,
398
- hasExistingState: true,
372
+ ...availability,
373
+ resume: {
374
+ available: false,
375
+ reason: error.message,
376
+ },
399
377
  details: `Interrupted run found, but resume is unavailable.\n${error.message}`,
400
378
  };
401
379
  }
402
380
  }
381
+ if (directState && availability.continue.available) {
382
+ return {
383
+ ...availability,
384
+ details: buildFlowContinueDetails(directState),
385
+ };
386
+ }
403
387
  return {
404
- resumeAvailable: false,
405
- hasExistingState: Boolean(directState),
388
+ ...availability,
406
389
  };
407
390
  }
408
- function printAutoPhasesHelp() {
409
- const phaseLines = ["Available auto-golang phases:", "", ...autoPhaseIds()];
391
+ async function printAutoPhasesHelp() {
392
+ const phaseLines = ["Available auto-golang phases:", "", ...(await autoPhaseIds())];
410
393
  phaseLines.push("", "You can resume auto-golang from a phase with:", "agentweaver auto-golang --from <phase> <jira>", "or in interactive mode:", "/auto-golang --from <phase>");
411
394
  printPanel("Auto-Golang Phases", phaseLines.join("\n"), "magenta");
412
395
  }
413
- function autoCommonPhaseIds() {
414
- return loadDeclarativeFlow({ source: "built-in", fileName: "auto-common.json" }).phases.map((phase) => phase.id);
396
+ async function autoCommonPhaseIds() {
397
+ return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-common.json" })).phases.map((phase) => phase.id);
415
398
  }
416
- function printAutoCommonPhasesHelp() {
417
- const phaseLines = ["Available auto-common phases:", "", ...autoCommonPhaseIds()];
399
+ async function printAutoCommonPhasesHelp() {
400
+ const phaseLines = ["Available auto-common phases:", "", ...(await autoCommonPhaseIds())];
418
401
  phaseLines.push("", "You can run auto-common with:", "agentweaver auto-common <jira>");
419
402
  printPanel("Auto-Common Phases", phaseLines.join("\n"), "magenta");
420
403
  }
404
+ async function autoSimplePhaseIds() {
405
+ return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-simple.json" })).phases.map((phase) => phase.id);
406
+ }
407
+ async function printAutoSimplePhasesHelp() {
408
+ const phaseLines = ["Available auto-simple phases:", "", ...(await autoSimplePhaseIds())];
409
+ phaseLines.push("", "You can run auto-simple with:", "agentweaver auto-simple <jira>");
410
+ printPanel("Auto-Simple Phases", phaseLines.join("\n"), "magenta");
411
+ }
421
412
  function nextReviewIterationForTask(taskKey) {
422
413
  return nextArtifactIteration(taskKey, "review");
423
414
  }
@@ -430,8 +421,9 @@ function buildBaseConfig(command, options = {}) {
430
421
  jiraRef: options.jiraRef ?? null,
431
422
  scopeName: options.scopeName ?? null,
432
423
  reviewFixPoints: options.reviewFixPoints ?? null,
424
+ reviewBlockingSeverities: options.reviewBlockingSeverities ?? resolveReviewBlockingSeveritiesFromEnv(),
433
425
  extraPrompt: options.extraPrompt ?? null,
434
- autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
426
+ autoFromPhase: options.autoFromPhase ?? null,
435
427
  mdLang: options.mdLang ?? null,
436
428
  dryRun: options.dryRun ?? false,
437
429
  verbose: options.verbose ?? false,
@@ -439,21 +431,23 @@ function buildBaseConfig(command, options = {}) {
439
431
  };
440
432
  }
441
433
  function commandRequiresTask(command) {
442
- return (command === "plan" ||
443
- command === "plan-revise" ||
434
+ return (command === "plan-revise" ||
444
435
  command === "bug-analyze" ||
445
436
  command === "bug-fix" ||
446
437
  command === "design-review" ||
447
438
  command === "mr-description" ||
448
439
  command === "auto-golang" ||
449
440
  command === "auto-common" ||
441
+ command === "auto-simple" ||
450
442
  command === "auto-status" ||
451
443
  command === "auto-reset");
452
444
  }
453
445
  function commandSupportsProjectScope(command) {
454
- return (command === "git-commit" ||
446
+ return (command === "plan" ||
447
+ command === "git-commit" ||
455
448
  command === "gitlab-diff-review" ||
456
449
  command === "gitlab-review" ||
450
+ command === "instant-task" ||
457
451
  command === "task-describe" ||
458
452
  command === "implement" ||
459
453
  command === "review" ||
@@ -463,9 +457,21 @@ function commandSupportsProjectScope(command) {
463
457
  command === "run-go-linter-loop");
464
458
  }
465
459
  async function resolveScopeForCommand(config, requestUserInput) {
460
+ if (config.command === "instant-task") {
461
+ if (config.scopeName?.trim()) {
462
+ throw new TaskRunnerError("Command 'instant-task' rejects explicit scope overrides. The current branch-derived scope is the only supported lineage identity.");
463
+ }
464
+ if (config.jiraRef?.trim()) {
465
+ throw new TaskRunnerError("Command 'instant-task' does not accept a Jira task argument. Start it without a positional Jira reference.");
466
+ }
467
+ return resolveProjectScope();
468
+ }
466
469
  if (config.jiraRef?.trim()) {
467
470
  return resolveProjectScope(config.scopeName, config.jiraRef);
468
471
  }
472
+ if (config.command === "plan") {
473
+ return resolveProjectScope(config.scopeName);
474
+ }
469
475
  if (commandRequiresTask(config.command)) {
470
476
  try {
471
477
  const jiraContext = await requestJiraContext(requestUserInput);
@@ -496,27 +502,74 @@ function buildRuntimeConfig(baseConfig, scope) {
496
502
  ...(scope.jiraTaskFile ? { jiraTaskFile: scope.jiraTaskFile } : {}),
497
503
  };
498
504
  }
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") {
505
+ function routingForPrerequisites(launchProfile, executionRouting) {
506
+ if (executionRouting) {
507
+ return executionRouting;
508
+ }
509
+ return resolveExecutionRouting({
510
+ defaultRoute: launchProfile
511
+ ? {
512
+ executor: launchProfile.executor,
513
+ model: launchProfile.model,
514
+ }
515
+ : {
516
+ executor: DEFAULT_LAUNCH_PROFILE.executor,
517
+ model: DEFAULT_LAUNCH_PROFILE.model,
518
+ },
519
+ });
520
+ }
521
+ function flowSpecFileForPrerequisiteChecks(command) {
522
+ return isBuiltInCommandFlowId(command) ? builtInCommandFlowFile(command) : null;
523
+ }
524
+ async function commandRoutingGroupsForPrerequisiteChecks(command, cwd) {
525
+ const fileName = flowSpecFileForPrerequisiteChecks(command);
526
+ if (!fileName) {
527
+ return [];
528
+ }
529
+ return collectFlowRoutingGroups(await loadDeclarativeFlow({ source: "built-in", fileName }), cwd);
530
+ }
531
+ function resolveExecutorPrerequisite(executor, registryContext) {
532
+ if (executor === "codex") {
508
533
  resolveCmd("codex", "CODEX_BIN");
534
+ return;
535
+ }
536
+ if (executor === "opencode") {
537
+ resolveCmd("opencode", "OPENCODE_BIN");
538
+ return;
539
+ }
540
+ const definition = registryContext.executors.get(executor);
541
+ const config = definition.defaultConfig;
542
+ if (config
543
+ && typeof config === "object"
544
+ && !Array.isArray(config)
545
+ && typeof config.defaultCommand === "string"
546
+ && typeof config.commandEnvVar === "string") {
547
+ resolveCmd(config.defaultCommand, config.commandEnvVar);
509
548
  }
510
549
  }
511
- function checkAutoPrerequisites(config) {
512
- resolveCmd("codex", "CODEX_BIN");
550
+ async function checkPrerequisites(config, launchProfile, executionRouting) {
551
+ const registryContext = await createPipelineRegistryContext(process.cwd());
552
+ const routing = routingForPrerequisites(launchProfile, executionRouting);
553
+ const groups = await commandRoutingGroupsForPrerequisiteChecks(config.command, process.cwd());
554
+ for (const executor of executorsForRoutingGroups(routing, groups)) {
555
+ resolveExecutorPrerequisite(executor, registryContext);
556
+ }
557
+ }
558
+ async function checkAutoPrerequisites(config, launchProfile, executionRouting) {
559
+ await checkPrerequisites(config, launchProfile, executionRouting);
513
560
  }
514
561
  function autoFlowParams(config, forceRefreshSummary = false) {
515
562
  return {
516
563
  jiraApiUrl: config.jiraApiUrl,
517
564
  taskKey: config.taskKey,
565
+ taskContextIteration: nextArtifactIteration(config.taskKey, "task-context", "json"),
566
+ taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
567
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
568
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
569
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
518
570
  extraPrompt: config.extraPrompt,
519
571
  reviewFixPoints: config.reviewFixPoints,
572
+ reviewBlockingSeverities: config.reviewBlockingSeverities,
520
573
  forceRefresh: forceRefreshSummary,
521
574
  mdLang: config.mdLang,
522
575
  runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
@@ -525,6 +578,72 @@ function autoFlowParams(config, forceRefreshSummary = false) {
525
578
  runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
526
579
  };
527
580
  }
581
+ function reviewFlowParamsFromContract(config) {
582
+ const contract = resolveReviewInputContract(config.taskKey);
583
+ return {
584
+ taskKey: config.taskKey,
585
+ planningIteration: contract.planningIteration,
586
+ designFile: contract.designFile,
587
+ designJsonFile: contract.designJsonFile,
588
+ planFile: contract.planFile,
589
+ planJsonFile: contract.planJsonFile,
590
+ hasTaskContextJsonFile: contract.hasTaskContextJsonFile,
591
+ taskContextJsonFilePath: contract.taskContextJsonFilePath,
592
+ taskContextJsonFile: contract.taskContextJsonFile,
593
+ hasJiraTaskFile: contract.hasJiraTaskFile,
594
+ jiraTaskFilePath: contract.jiraTaskFilePath,
595
+ jiraTaskFile: contract.jiraTaskFile,
596
+ hasTaskInputJsonFile: contract.hasTaskInputJsonFile,
597
+ taskInputJsonFilePath: contract.taskInputJsonFilePath,
598
+ taskInputJsonFile: contract.taskInputJsonFile,
599
+ };
600
+ }
601
+ function hasStructuredReviewInputs(taskKey) {
602
+ const inspection = inspectReviewInputContract(taskKey);
603
+ if (inspection.status === "ready") {
604
+ return true;
605
+ }
606
+ if (inspection.status === "missing-planning") {
607
+ return false;
608
+ }
609
+ throw new TaskRunnerError(`Structured review requires a normalized task-context artifact, or legacy Jira/instant-task context, in scope '${taskKey}'.`);
610
+ }
611
+ function latestTaskContextIteration(taskKey) {
612
+ const iteration = latestArtifactIteration(taskKey, "task-context", "json");
613
+ if (iteration === null) {
614
+ throw new TaskRunnerError(`Plan mode requires a normalized task-context artifact in scope '${taskKey}'.`);
615
+ }
616
+ return iteration;
617
+ }
618
+ function loadInstantTaskInputDefaults(taskKey) {
619
+ const artifactPath = instantTaskInputJsonFile(taskKey);
620
+ if (!existsSync(artifactPath)) {
621
+ return null;
622
+ }
623
+ try {
624
+ validateStructuredArtifacts([{ path: artifactPath, schemaId: "user-input/v1" }], "Instant-task source input structured artifact is invalid.");
625
+ const parsed = JSON.parse(readFileSync(artifactPath, "utf8"));
626
+ const values = parsed.values;
627
+ if (!values || typeof values !== "object" || Array.isArray(values)) {
628
+ return null;
629
+ }
630
+ const normalizedEntries = [];
631
+ for (const [key, value] of Object.entries(values)) {
632
+ if (typeof value === "string" || typeof value === "boolean") {
633
+ normalizedEntries.push([key, value]);
634
+ continue;
635
+ }
636
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
637
+ normalizedEntries.push([key, [...value]]);
638
+ continue;
639
+ }
640
+ }
641
+ return Object.fromEntries(normalizedEntries);
642
+ }
643
+ catch {
644
+ return null;
645
+ }
646
+ }
528
647
  function interactiveFlowDefinition(entry) {
529
648
  const flow = entry.flow;
530
649
  return {
@@ -533,7 +652,7 @@ function interactiveFlowDefinition(entry) {
533
652
  description: flow.description ?? "No description available for this flow.",
534
653
  source: entry.source,
535
654
  treePath: [...entry.treePath],
536
- ...(entry.source === "project-local" ? { sourcePath: entry.absolutePath } : {}),
655
+ ...(entry.source !== "built-in" ? { sourcePath: entry.absolutePath } : {}),
537
656
  phases: flow.phases.map((phase) => ({
538
657
  id: phase.id,
539
658
  repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
@@ -583,7 +702,7 @@ function findCurrentFlowExecutionStep(state) {
583
702
  return null;
584
703
  }
585
704
  async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, overrides = {}, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
586
- const context = createPipelineContext({
705
+ const context = await createPipelineContext({
587
706
  issueKey: config.taskKey,
588
707
  jiraRef: config.jiraRef,
589
708
  dryRun: config.dryRun,
@@ -592,35 +711,57 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
592
711
  runtime,
593
712
  ...(setSummary ? { setSummary } : {}),
594
713
  requestUserInput,
714
+ ...(overrides.executionRouting ? { executionRouting: overrides.executionRouting } : {}),
595
715
  });
596
- const flow = loadDeclarativeFlow(flowRef);
716
+ const flow = await loadDeclarativeFlow(flowRef);
597
717
  const initialExecutionState = {
598
718
  flowKind: flow.kind,
599
719
  flowVersion: flow.version,
600
720
  terminated: false,
721
+ terminationOutcome: "success",
601
722
  phases: [],
602
723
  };
603
- let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
724
+ const existingStateForRestart = launchMode === "restart" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
725
+ let persistedState = launchMode === "resume" || launchMode === "continue"
726
+ ? loadFlowRunState(config.scope.scopeKey, flowId)
727
+ : null;
604
728
  if (persistedState && launchMode === "resume") {
605
- validateDeclarativeFlowResumeState({
729
+ await validateDeclarativeFlowResumeState({
606
730
  id: flowId,
607
731
  source: flow.source,
608
732
  fileName: flow.fileName,
609
733
  absolutePath: flow.absolutePath,
610
734
  treePath: [],
611
735
  flow,
612
- }, config, persistedState, overrides.launchProfile, runtime);
736
+ }, config, persistedState, overrides.executionRouting ?? (overrides.launchProfile ? resolveExecutionRouting({ defaultRoute: {
737
+ executor: overrides.launchProfile.executor,
738
+ model: overrides.launchProfile.model,
739
+ } }) : undefined), runtime);
613
740
  persistedState = prepareFlowStateForResume(persistedState);
614
741
  }
742
+ else if (persistedState && launchMode === "continue") {
743
+ persistedState = prepareFlowStateForContinue(persistedState, flow.phases);
744
+ }
615
745
  else if (launchMode === "restart") {
746
+ if (existingStateForRestart) {
747
+ archiveActiveAttempt(config.scope.scopeKey);
748
+ }
616
749
  resetFlowRunState(config.scope.scopeKey, flowId);
617
750
  }
618
751
  const executionState = persistedState?.executionState ?? initialExecutionState;
619
752
  const state = persistedState
620
- ?? createFlowRunState(config.scope.scopeKey, flowId, executionState, config.jiraRef, overrides.launchProfile);
621
- if (overrides.launchProfile) {
753
+ ?? createFlowRunState(config.scope.scopeKey, flowId, executionState, config.jiraRef, overrides.launchProfile, overrides.executionRouting, overrides.selectedRoutingPreset);
754
+ if (overrides.executionRouting) {
755
+ state.executionRouting = overrides.executionRouting;
756
+ state.routingFingerprint = overrides.executionRouting.fingerprint;
757
+ state.launchProfile = overrides.executionRouting.defaultRoute;
758
+ }
759
+ else if (overrides.launchProfile) {
622
760
  state.launchProfile = overrides.launchProfile;
623
761
  }
762
+ if (overrides.selectedRoutingPreset) {
763
+ state.selectedRoutingPreset = overrides.selectedRoutingPreset;
764
+ }
624
765
  state.status = "running";
625
766
  state.lastError = null;
626
767
  state.currentStep = findCurrentFlowExecutionStep(state);
@@ -645,7 +786,12 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
645
786
  },
646
787
  });
647
788
  }
648
- state.status = "completed";
789
+ if (executionState.terminated) {
790
+ state.status = executionState.terminationOutcome === "success" ? "completed" : "blocked";
791
+ }
792
+ else {
793
+ state.status = "completed";
794
+ }
649
795
  state.currentStep = null;
650
796
  state.lastError = null;
651
797
  state.executionState = executionState;
@@ -671,12 +817,24 @@ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, overri
671
817
  ...defaultDeclarativeFlowParams(config, false, overrides),
672
818
  ...flowParams,
673
819
  };
674
- await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, mergedFlowParams, overrides, requestUserInput, setSummary, launchMode, runtime);
820
+ await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, withCanonicalReviewLoopParams((await loadDeclarativeFlow({ source: "built-in", fileName })).kind, mergedFlowParams), overrides, requestUserInput, setSummary, launchMode, runtime);
675
821
  }
676
822
  function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overrides = {}) {
677
823
  const iteration = nextReviewIterationForTask(config.taskKey);
678
824
  const latestIteration = latestArtifactIteration(config.taskKey, "review");
679
- const launchProfile = overrides.launchProfile ?? resolveLaunchProfile({ executor: "default", model: "default" }, DEFAULT_LAUNCH_PROFILE);
825
+ const latestTaskContext = latestArtifactIteration(config.taskKey, "task-context", "json");
826
+ const executionRouting = overrides.executionRouting ?? resolveExecutionRouting({
827
+ defaultRoute: overrides.launchProfile
828
+ ? {
829
+ executor: overrides.launchProfile.executor,
830
+ model: overrides.launchProfile.model,
831
+ }
832
+ : {
833
+ executor: DEFAULT_LAUNCH_PROFILE.executor,
834
+ model: DEFAULT_LAUNCH_PROFILE.model,
835
+ },
836
+ });
837
+ const launchProfile = executionRouting.defaultRoute;
680
838
  return {
681
839
  taskKey: config.taskKey,
682
840
  jiraRef: config.jiraRef,
@@ -687,12 +845,17 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
687
845
  workspaceDir: scopeWorkspaceDir(config.taskKey),
688
846
  extraPrompt: config.extraPrompt,
689
847
  reviewFixPoints: config.reviewFixPoints,
848
+ reviewBlockingSeverities: config.reviewBlockingSeverities,
690
849
  mdLang: config.mdLang,
691
850
  llmExecutor: launchProfile.executor,
692
851
  llmModel: launchProfile.model,
693
852
  launchProfile,
853
+ executionRouting,
694
854
  iteration,
855
+ baseIteration: iteration,
856
+ designReviewBaseIteration: nextDesignReviewIterationForTask(config.taskKey),
695
857
  latestIteration,
858
+ taskContextIteration: latestTaskContext ?? nextArtifactIteration(config.taskKey, "task-context", "json"),
696
859
  taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
697
860
  designIteration: nextArtifactIteration(config.taskKey, "design"),
698
861
  planIteration: nextArtifactIteration(config.taskKey, "plan"),
@@ -701,6 +864,60 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
701
864
  forceRefresh: forceRefreshSummary,
702
865
  };
703
866
  }
867
+ function countAvailableNonRestartActions(availability) {
868
+ return Number(availability.resume.available) + Number(availability.continue.available);
869
+ }
870
+ async function chooseLaunchMode(flowId, scopeKey, explicitLaunchMode, requestUserInput) {
871
+ const state = loadFlowRunState(scopeKey, flowId);
872
+ const availability = classifyFlowLaunchAvailability(state);
873
+ if (explicitLaunchMode) {
874
+ const selectedAvailability = availability[explicitLaunchMode];
875
+ if (!selectedAvailability.available) {
876
+ throw new TaskRunnerError(`${explicitLaunchMode.charAt(0).toUpperCase()}${explicitLaunchMode.slice(1)} is not available for '${flowId}'. ${selectedAvailability.reason}`);
877
+ }
878
+ return explicitLaunchMode;
879
+ }
880
+ if (!availability.hasExistingState) {
881
+ return "restart";
882
+ }
883
+ const availableNonRestart = countAvailableNonRestartActions(availability);
884
+ if (availableNonRestart === 0) {
885
+ return "restart";
886
+ }
887
+ const interactive = requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
888
+ if (!interactive) {
889
+ throw new TaskRunnerError(`Multiple actions are valid for '${flowId}'. Re-run with one of: --resume, --continue, --restart.`);
890
+ }
891
+ const result = await requestUserInput({
892
+ formId: `launch-mode-${flowId}`,
893
+ title: "Launch Action",
894
+ description: `Select how to start '${flowId}'.`,
895
+ submitLabel: "Start",
896
+ fields: [
897
+ {
898
+ id: "launchMode",
899
+ type: "single-select",
900
+ label: "Action",
901
+ required: true,
902
+ default: availability.continue.available ? "continue" : availability.resume.available ? "resume" : "restart",
903
+ options: [
904
+ ...(availability.resume.available
905
+ ? [{ value: "resume", label: "Resume", description: availability.resume.reason }]
906
+ : []),
907
+ ...(availability.continue.available
908
+ ? [{ value: "continue", label: "Continue", description: availability.continue.reason }]
909
+ : []),
910
+ { value: "restart", label: "Restart", description: availability.restart.reason },
911
+ ],
912
+ },
913
+ ],
914
+ });
915
+ const selected = result.values.launchMode;
916
+ if (selected !== "resume" && selected !== "continue" && selected !== "restart") {
917
+ throw new TaskRunnerError(`Invalid launch action selected for '${flowId}'.`);
918
+ }
919
+ return selected;
920
+ }
704
921
  const TASK_SCOPE_PARAM_REFS = new Set(["params.jiraApiUrl", "params.jiraBrowseUrl", "params.jiraTaskFile"]);
705
922
  function valueReferencesTaskScopeParams(value) {
706
923
  if (Array.isArray(value)) {
@@ -723,7 +940,7 @@ function flowRequiresTaskScope(entry) {
723
940
  return valueReferencesTaskScopeParams(entry.flow.phases);
724
941
  }
725
942
  async function summarizeBuildFailure(output) {
726
- return summarizeBuildFailureViaPipeline(createPipelineContext({
943
+ return summarizeBuildFailureViaPipeline(await createPipelineContext({
727
944
  issueKey: "build-failure-summary",
728
945
  jiraRef: "build-failure-summary",
729
946
  dryRun: false,
@@ -738,22 +955,56 @@ function requireJiraConfig(config) {
738
955
  throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
739
956
  }
740
957
  }
741
- async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", launchProfile, runtime = runtimeServices) {
958
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, explicitLaunchMode, launchProfile, executionRouting, selectedRoutingPreset, runtime = runtimeServices) {
742
959
  if (baseConfig.command === "doctor") {
743
960
  const exitCode = await runDoctorCommand(baseConfig.doctorArgs ?? []);
744
961
  return exitCode === 0;
745
962
  }
746
963
  const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
964
+ const flowOverrides = executionRouting
965
+ ? {
966
+ launchProfile: executionRouting.defaultRoute,
967
+ executionRouting,
968
+ ...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
969
+ }
970
+ : launchProfile
971
+ ? { launchProfile }
972
+ : {};
973
+ const launchMode = config.command === "auto-status" || config.command === "auto-reset"
974
+ ? "restart"
975
+ : await chooseLaunchMode(config.command, config.scope.scopeKey, explicitLaunchMode, requestUserInput);
976
+ if (config.command === "instant-task") {
977
+ await checkPrerequisites(config, launchProfile, executionRouting);
978
+ const hasPersistedInstantTaskState = loadFlowRunState(config.scope.scopeKey, "instant-task") !== null;
979
+ const repromptInstantTaskInput = launchMode === "restart"
980
+ && hasPersistedInstantTaskState
981
+ && requestUserInput !== requestUserInputInTerminal;
982
+ await runDeclarativeFlowBySpecFile("instant-task.json", config, {
983
+ taskKey: config.taskKey,
984
+ taskContextIteration: nextArtifactIteration(config.taskKey, "task-context", "json"),
985
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
986
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
987
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
988
+ extraPrompt: config.extraPrompt,
989
+ mdLang: config.mdLang,
990
+ repromptInstantTaskInput,
991
+ ...(repromptInstantTaskInput
992
+ ? { prefilledInstantTaskInputValues: loadInstantTaskInputDefaults(config.taskKey) ?? undefined }
993
+ : {}),
994
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
995
+ return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
996
+ }
747
997
  if (config.command === "auto-golang") {
748
998
  requireJiraConfig(config);
749
- checkAutoPrerequisites(config);
750
999
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
751
1000
  process.env.JIRA_API_URL = config.jiraApiUrl;
752
1001
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
753
1002
  let effectiveLaunchMode = launchMode;
754
1003
  let effectiveLaunchProfile = launchProfile;
1004
+ let effectiveExecutionRouting = executionRouting;
755
1005
  if (config.autoFromPhase) {
756
- const flow = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
1006
+ config.autoFromPhase = await validateAutoPhaseId(config.autoFromPhase);
1007
+ const flow = await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
757
1008
  const persistedState = loadFlowRunState(config.scope.scopeKey, "auto-golang");
758
1009
  if (!persistedState) {
759
1010
  throw new TaskRunnerError(`Cannot restart auto-golang from phase '${config.autoFromPhase}' because persisted flow state was not found.`);
@@ -762,18 +1013,37 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
762
1013
  saveFlowRunState(persistedState);
763
1014
  effectiveLaunchMode = "resume";
764
1015
  effectiveLaunchProfile ??= persistedState.launchProfile;
1016
+ effectiveExecutionRouting ??= persistedState.executionRouting;
765
1017
  printPanel("Auto-Golang Resume", `Auto-golang pipeline will continue from phase: ${config.autoFromPhase}`, "yellow");
766
1018
  }
767
- await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveLaunchProfile ? { launchProfile: effectiveLaunchProfile } : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
1019
+ await checkAutoPrerequisites(config, effectiveLaunchProfile, effectiveExecutionRouting);
1020
+ await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveExecutionRouting
1021
+ ? {
1022
+ launchProfile: effectiveExecutionRouting.defaultRoute,
1023
+ executionRouting: effectiveExecutionRouting,
1024
+ ...(selectedRoutingPreset ? { selectedRoutingPreset } : {}),
1025
+ }
1026
+ : effectiveLaunchProfile
1027
+ ? { launchProfile: effectiveLaunchProfile }
1028
+ : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
768
1029
  return false;
769
1030
  }
770
1031
  if (config.command === "auto-common") {
771
1032
  requireJiraConfig(config);
772
- checkAutoPrerequisites(config);
1033
+ await checkAutoPrerequisites(config, launchProfile, executionRouting);
773
1034
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
774
1035
  process.env.JIRA_API_URL = config.jiraApiUrl;
775
1036
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
776
- await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
1037
+ await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1038
+ return false;
1039
+ }
1040
+ if (config.command === "auto-simple") {
1041
+ requireJiraConfig(config);
1042
+ await checkAutoPrerequisites(config, launchProfile, executionRouting);
1043
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
1044
+ process.env.JIRA_API_URL = config.jiraApiUrl;
1045
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile;
1046
+ await runDeclarativeFlowBySpecFile("auto-simple.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
777
1047
  return false;
778
1048
  }
779
1049
  if (config.command === "auto-status") {
@@ -783,14 +1053,18 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
783
1053
  return false;
784
1054
  }
785
1055
  const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
786
- const phaseOrder = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases;
1056
+ const phaseOrder = (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" })).phases;
787
1057
  const lines = [
788
1058
  `Issue: ${config.taskKey}`,
789
1059
  `Status: ${state.status}`,
790
1060
  `Current step: ${currentStep}`,
791
1061
  `Updated: ${state.updatedAt}`,
792
1062
  ];
793
- if (state.launchProfile) {
1063
+ if (state.executionRouting) {
1064
+ lines.push(`Default route: ${state.executionRouting.defaultRoute.executor} / ${state.executionRouting.defaultRoute.model}`);
1065
+ lines.push(`Routing fingerprint: ${state.executionRouting.fingerprint}`);
1066
+ }
1067
+ else if (state.launchProfile) {
794
1068
  lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
795
1069
  }
796
1070
  if (state.lastError) {
@@ -816,7 +1090,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
816
1090
  printPanel("Auto-Golang Reset", removed ? `State file ${flowStateFile(config.scope.scopeKey, "auto-golang")} removed.` : "No flow state file found.", "yellow");
817
1091
  return false;
818
1092
  }
819
- checkPrerequisites(config);
1093
+ await checkPrerequisites(config, launchProfile, executionRouting);
820
1094
  if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
821
1095
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
822
1096
  process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
@@ -828,22 +1102,37 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
828
1102
  delete process.env.JIRA_TASK_FILE;
829
1103
  }
830
1104
  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`);
1105
+ let taskContextIteration;
1106
+ if (config.jiraRef) {
1107
+ requireJiraConfig(config);
1108
+ if (config.verbose) {
1109
+ process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
1110
+ process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
1111
+ process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
1112
+ }
1113
+ taskContextIteration = nextArtifactIteration(config.taskKey, "task-context", "json");
1114
+ await runDeclarativeFlowBySpecFile("task-source/jira-fetch.json", config, {
1115
+ jiraApiUrl: config.jiraApiUrl,
1116
+ taskKey: config.taskKey,
1117
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1118
+ await runDeclarativeFlowBySpecFile("normalize-task-source.json", config, {
1119
+ taskKey: config.taskKey,
1120
+ iteration: taskContextIteration,
1121
+ extraPrompt: config.extraPrompt,
1122
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1123
+ }
1124
+ else {
1125
+ taskContextIteration = latestTaskContextIteration(config.taskKey);
836
1126
  }
837
1127
  await runDeclarativeFlowBySpecFile("plan.json", config, {
838
- jiraApiUrl: config.jiraApiUrl,
839
1128
  taskKey: config.taskKey,
840
- taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
1129
+ taskContextIteration,
841
1130
  designIteration: nextArtifactIteration(config.taskKey, "design"),
842
1131
  planIteration: nextArtifactIteration(config.taskKey, "plan"),
843
1132
  qaIteration: nextArtifactIteration(config.taskKey, "qa"),
844
1133
  extraPrompt: config.extraPrompt,
845
1134
  forceRefresh: forceRefreshSummary,
846
- }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
1135
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
847
1136
  return false;
848
1137
  }
849
1138
  if (config.command === "bug-analyze") {
@@ -862,7 +1151,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
862
1151
  bugFixPlanIteration: nextArtifactIteration(config.taskKey, "bug-fix-plan"),
863
1152
  extraPrompt: config.extraPrompt,
864
1153
  forceRefresh: forceRefreshSummary,
865
- }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
1154
+ }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
866
1155
  return false;
867
1156
  }
868
1157
  if (config.command === "design-review") {
@@ -884,6 +1173,9 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
884
1173
  qaJsonFilePath: inputContract.qaJsonFilePath,
885
1174
  qaFile: inputContract.qaFile,
886
1175
  qaJsonFile: inputContract.qaJsonFile,
1176
+ hasTaskContextJsonFile: inputContract.hasTaskContextJsonFile,
1177
+ taskContextJsonFilePath: inputContract.taskContextJsonFilePath,
1178
+ taskContextJsonFile: inputContract.taskContextJsonFile,
887
1179
  hasJiraTaskFile: inputContract.hasJiraTaskFile,
888
1180
  jiraTaskFilePath: inputContract.jiraTaskFilePath,
889
1181
  jiraTaskFile: inputContract.jiraTaskFile,
@@ -896,8 +1188,11 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
896
1188
  hasPlanningAnswersJsonFile: inputContract.hasPlanningAnswersJsonFile,
897
1189
  planningAnswersJsonFilePath: inputContract.planningAnswersJsonFilePath,
898
1190
  planningAnswersJsonFile: inputContract.planningAnswersJsonFile,
1191
+ hasTaskInputJsonFile: inputContract.hasTaskInputJsonFile,
1192
+ taskInputJsonFilePath: inputContract.taskInputJsonFilePath,
1193
+ taskInputJsonFile: inputContract.taskInputJsonFile,
899
1194
  extraPrompt: config.extraPrompt,
900
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1195
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
901
1196
  if (!config.dryRun) {
902
1197
  printSummary("Design Review", `Artifacts:\n${designReviewFile(config.taskKey, iteration)}\n${designReviewJsonFile(config.taskKey, iteration)}`);
903
1198
  }
@@ -930,6 +1225,9 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
930
1225
  revisedPlanJsonFile: inputContract.revisedPlanJsonFile,
931
1226
  revisedQaFile: inputContract.revisedQaFile,
932
1227
  revisedQaJsonFile: inputContract.revisedQaJsonFile,
1228
+ hasTaskContextJsonFile: inputContract.hasTaskContextJsonFile,
1229
+ taskContextJsonFilePath: inputContract.taskContextJsonFilePath,
1230
+ taskContextJsonFile: inputContract.taskContextJsonFile,
933
1231
  hasJiraTaskFile: inputContract.hasJiraTaskFile,
934
1232
  jiraTaskFilePath: inputContract.jiraTaskFilePath,
935
1233
  jiraTaskFile: inputContract.jiraTaskFile,
@@ -942,8 +1240,11 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
942
1240
  hasPlanningAnswersJsonFile: inputContract.hasPlanningAnswersJsonFile,
943
1241
  planningAnswersJsonFilePath: inputContract.planningAnswersJsonFilePath,
944
1242
  planningAnswersJsonFile: inputContract.planningAnswersJsonFile,
1243
+ hasTaskInputJsonFile: inputContract.hasTaskInputJsonFile,
1244
+ taskInputJsonFilePath: inputContract.taskInputJsonFilePath,
1245
+ taskInputJsonFile: inputContract.taskInputJsonFile,
945
1246
  extraPrompt: config.extraPrompt,
946
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1247
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
947
1248
  if (!config.dryRun) {
948
1249
  printSummary("Plan Revise", `Artifacts:\n${inputContract.revisedDesignFile}\n${inputContract.revisedDesignJsonFile}\n${inputContract.revisedPlanFile}\n${inputContract.revisedPlanJsonFile}\n${inputContract.revisedQaFile}\n${inputContract.revisedQaJsonFile}`);
949
1250
  }
@@ -959,7 +1260,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
959
1260
  extraPrompt: config.extraPrompt,
960
1261
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, iteration),
961
1262
  reviewFixPoints: config.reviewFixPoints,
962
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1263
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
963
1264
  if (!config.dryRun) {
964
1265
  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)}`);
965
1266
  }
@@ -973,7 +1274,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
973
1274
  iteration,
974
1275
  gitlabDiffIteration,
975
1276
  extraPrompt: config.extraPrompt,
976
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1277
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
977
1278
  if (!config.dryRun) {
978
1279
  printSummary("GitLab Diff Review", `Artifacts:\n${gitlabDiffFile(config.taskKey)}\n${gitlabDiffJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}`);
979
1280
  }
@@ -991,7 +1292,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
991
1292
  await runDeclarativeFlowBySpecFile("bugz/bug-fix.json", config, {
992
1293
  taskKey: config.taskKey,
993
1294
  extraPrompt: config.extraPrompt,
994
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1295
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
995
1296
  return false;
996
1297
  }
997
1298
  if (config.command === "mr-description") {
@@ -1001,7 +1302,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1001
1302
  taskKey: config.taskKey,
1002
1303
  iteration: nextArtifactIteration(config.taskKey, "mr-description"),
1003
1304
  extraPrompt: config.extraPrompt,
1004
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1305
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1005
1306
  return false;
1006
1307
  }
1007
1308
  if (config.command === "task-describe") {
@@ -1011,42 +1312,33 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1011
1312
  taskKey: config.taskKey,
1012
1313
  iteration,
1013
1314
  extraPrompt: config.extraPrompt,
1014
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1315
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1015
1316
  return false;
1016
1317
  }
1017
1318
  if (config.command === "implement") {
1018
- requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
1019
- validateStructuredArtifacts([
1020
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
1021
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
1022
- { path: qaJsonFile(config.taskKey), schemaId: "qa-plan/v1" },
1023
- ], "Implement mode requires valid structured plan artifacts from the planning phase.");
1319
+ const planningBundle = resolveLatestPlanningBundle(config.taskKey);
1024
1320
  await runDeclarativeFlowBySpecFile("implement.json", config, {
1025
1321
  taskKey: config.taskKey,
1322
+ planningIteration: planningBundle.planningIteration,
1026
1323
  extraPrompt: config.extraPrompt,
1027
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode);
1324
+ }, flowOverrides, requestUserInput, undefined, launchMode);
1028
1325
  return false;
1029
1326
  }
1030
1327
  if (config.command === "review") {
1031
1328
  const iteration = nextReviewIterationForTask(config.taskKey);
1032
- if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
1033
- requireJiraConfig(config);
1034
- validateStructuredArtifacts([
1035
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
1036
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
1037
- ], "Review mode requires valid structured plan artifacts from the planning phase.");
1329
+ if (hasStructuredReviewInputs(config.taskKey)) {
1038
1330
  await runDeclarativeFlowBySpecFile("review/review.json", config, {
1039
- taskKey: config.taskKey,
1331
+ ...reviewFlowParamsFromContract(config),
1040
1332
  iteration,
1041
1333
  extraPrompt: config.extraPrompt,
1042
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1334
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1043
1335
  }
1044
1336
  else {
1045
1337
  await runDeclarativeFlowBySpecFile("review/review-project.json", config, {
1046
1338
  taskKey: config.taskKey,
1047
1339
  iteration,
1048
1340
  extraPrompt: config.extraPrompt,
1049
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1341
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1050
1342
  }
1051
1343
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
1052
1344
  }
@@ -1065,23 +1357,21 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1065
1357
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
1066
1358
  extraPrompt: config.extraPrompt,
1067
1359
  reviewFixPoints: config.reviewFixPoints,
1068
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1360
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1069
1361
  return false;
1070
1362
  }
1071
1363
  if (config.command === "review-loop") {
1072
1364
  const iteration = nextReviewIterationForTask(config.taskKey);
1073
- if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
1074
- requireJiraConfig(config);
1075
- validateStructuredArtifacts([
1076
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
1077
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
1078
- ], "Review-loop mode requires valid structured plan artifacts from the planning phase.");
1079
- }
1080
- await runDeclarativeFlowBySpecFile("review/review-loop.json", config, {
1081
- taskKey: config.taskKey,
1082
- iteration,
1365
+ const reviewLoopSpec = hasStructuredReviewInputs(config.taskKey)
1366
+ ? "review/review-loop.json"
1367
+ : "review/review-project-loop.json";
1368
+ await runDeclarativeFlowBySpecFile(reviewLoopSpec, config, {
1369
+ ...(reviewLoopSpec === "review/review-loop.json"
1370
+ ? reviewFlowParamsFromContract(config)
1371
+ : { taskKey: config.taskKey }),
1372
+ baseIteration: iteration,
1083
1373
  extraPrompt: config.extraPrompt,
1084
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1374
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1085
1375
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
1086
1376
  }
1087
1377
  if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
@@ -1092,19 +1382,19 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1092
1382
  runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
1093
1383
  runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
1094
1384
  extraPrompt: config.extraPrompt,
1095
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1385
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1096
1386
  return false;
1097
1387
  }
1098
1388
  if (config.command === "git-commit") {
1099
1389
  await runDeclarativeFlowBySpecFile("git-commit.json", config, {
1100
1390
  taskKey: config.taskKey,
1101
1391
  extraPrompt: config.extraPrompt,
1102
- }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1392
+ }, flowOverrides, requestUserInput, undefined, launchMode, runtime);
1103
1393
  return false;
1104
1394
  }
1105
1395
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
1106
1396
  }
1107
- function parseCliArgs(argv) {
1397
+ async function parseCliArgs(argv) {
1108
1398
  if (argv.includes("--version") || argv.includes("-v")) {
1109
1399
  process.stdout.write(`${packageVersion()}\n`);
1110
1400
  process.exit(0);
@@ -1127,9 +1417,11 @@ function parseCliArgs(argv) {
1127
1417
  let prompt;
1128
1418
  let autoFromPhase;
1129
1419
  let scopeName;
1420
+ let reviewBlockingSeverities;
1130
1421
  let helpPhases = false;
1131
1422
  let jiraRef;
1132
1423
  let mdLang;
1424
+ let launchMode;
1133
1425
  const doctorArgs = [];
1134
1426
  for (let index = 1; index < argv.length; index += 1) {
1135
1427
  const token = argv[index] ?? "";
@@ -1145,6 +1437,14 @@ function parseCliArgs(argv) {
1145
1437
  helpPhases = true;
1146
1438
  continue;
1147
1439
  }
1440
+ if (token === "--resume" || token === "--continue" || token === "--restart") {
1441
+ if (launchMode) {
1442
+ process.stderr.write("Error: --resume, --continue, and --restart are mutually exclusive.\n");
1443
+ process.exit(1);
1444
+ }
1445
+ launchMode = token.slice(2);
1446
+ continue;
1447
+ }
1148
1448
  if (token === "--prompt") {
1149
1449
  prompt = argv[index + 1];
1150
1450
  index += 1;
@@ -1155,6 +1455,15 @@ function parseCliArgs(argv) {
1155
1455
  index += 1;
1156
1456
  continue;
1157
1457
  }
1458
+ if (token === "--blocking-severities") {
1459
+ reviewBlockingSeverities = parseReviewSeverityCsv(argv[index + 1] ?? "");
1460
+ index += 1;
1461
+ continue;
1462
+ }
1463
+ if (token.startsWith("--blocking-severities=")) {
1464
+ reviewBlockingSeverities = parseReviewSeverityCsv(token.slice("--blocking-severities=".length));
1465
+ continue;
1466
+ }
1158
1467
  if (token === "--from") {
1159
1468
  autoFromPhase = argv[index + 1];
1160
1469
  index += 1;
@@ -1191,11 +1500,15 @@ function parseCliArgs(argv) {
1191
1500
  }
1192
1501
  }
1193
1502
  if (command === "auto-golang" && helpPhases) {
1194
- printAutoPhasesHelp();
1503
+ await printAutoPhasesHelp();
1195
1504
  process.exit(0);
1196
1505
  }
1197
1506
  if (command === "auto-common" && helpPhases) {
1198
- printAutoCommonPhasesHelp();
1507
+ await printAutoCommonPhasesHelp();
1508
+ process.exit(0);
1509
+ }
1510
+ if (command === "auto-simple" && helpPhases) {
1511
+ await printAutoSimplePhasesHelp();
1199
1512
  process.exit(0);
1200
1513
  }
1201
1514
  return {
@@ -1205,16 +1518,19 @@ function parseCliArgs(argv) {
1205
1518
  helpPhases,
1206
1519
  ...(jiraRef !== undefined ? { jiraRef } : {}),
1207
1520
  ...(scopeName !== undefined ? { scopeName } : {}),
1521
+ ...(reviewBlockingSeverities !== undefined ? { reviewBlockingSeverities } : {}),
1208
1522
  ...(prompt !== undefined ? { prompt } : {}),
1209
1523
  ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
1210
1524
  ...(mdLang !== undefined ? { mdLang } : {}),
1211
1525
  ...(doctorArgs.length > 0 ? { doctorArgs } : {}),
1526
+ ...(launchMode !== undefined ? { launchMode } : {}),
1212
1527
  };
1213
1528
  }
1214
1529
  function buildConfigFromArgs(args) {
1215
1530
  return buildBaseConfig(args.command, {
1216
1531
  ...(args.jiraRef !== undefined ? { jiraRef: args.jiraRef } : {}),
1217
1532
  ...(args.scopeName !== undefined ? { scopeName: args.scopeName } : {}),
1533
+ ...(args.reviewBlockingSeverities !== undefined ? { reviewBlockingSeverities: args.reviewBlockingSeverities } : {}),
1218
1534
  ...(args.prompt !== undefined ? { extraPrompt: args.prompt } : {}),
1219
1535
  ...(args.autoFromPhase !== undefined ? { autoFromPhase: args.autoFromPhase } : {}),
1220
1536
  ...(args.mdLang !== undefined ? { mdLang: args.mdLang } : {}),
@@ -1226,11 +1542,11 @@ function buildConfigFromArgs(args) {
1226
1542
  async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1227
1543
  let currentScope = resolveProjectScope(scopeName, jiraRef);
1228
1544
  const gitBranchName = detectGitBranchName();
1229
- const flowCatalog = loadInteractiveFlowCatalog(process.cwd());
1545
+ const flowCatalog = await loadInteractiveFlowCatalog(process.cwd());
1230
1546
  let activeAbortController = null;
1231
1547
  let activeFlowId = null;
1232
1548
  let exiting = false;
1233
- const ui = new InteractiveUi({
1549
+ const ui = createInteractiveSession({
1234
1550
  scopeKey: currentScope.scopeKey,
1235
1551
  jiraIssueKey: currentScope.jiraIssueKey ?? null,
1236
1552
  summaryText: "",
@@ -1243,7 +1559,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1243
1559
  if (!flowEntry) {
1244
1560
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1245
1561
  }
1246
- const resumeLookup = lookupInteractiveFlowResume(flowEntry, currentScope);
1562
+ const resumeLookup = await lookupInteractiveFlowResume(flowEntry, currentScope);
1247
1563
  return resumeLookup;
1248
1564
  },
1249
1565
  onRun: async (flowId, launchMode) => {
@@ -1255,16 +1571,23 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1255
1571
  if (!flowEntry) {
1256
1572
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1257
1573
  }
1574
+ const routingGroups = await flowRoutingGroups(flowEntry, process.cwd());
1258
1575
  const resumeState = launchMode === "resume" ? loadFlowRunState(currentScope.scopeKey, flowId) : null;
1259
1576
  if (resumeState) {
1260
1577
  currentScope = scopeWithRestoredJiraContext(currentScope, resumeState);
1261
1578
  }
1262
- const launchProfile = launchMode === "resume"
1263
- ? resumeState?.launchProfile
1264
- : await requestInteractiveLaunchProfile((form) => ui.requestUserInput(form));
1265
- if (!launchProfile) {
1266
- throw new TaskRunnerError("Resume is impossible because launch profile was not saved. Use restart.");
1579
+ const routingSelection = launchMode === "resume"
1580
+ ? (resumeState?.executionRouting
1581
+ ? {
1582
+ routing: resumeState.executionRouting,
1583
+ selectedPreset: resumeState.selectedRoutingPreset ?? { kind: "custom", label: "Saved routing" },
1584
+ }
1585
+ : null)
1586
+ : await requestInteractiveExecutionRouting(flowEntry, (form) => ui.requestUserInput(form));
1587
+ if (launchMode === "resume" && !routingSelection?.routing) {
1588
+ throw new TaskRunnerError("Resume is impossible because execution routing was not saved. Use restart.");
1267
1589
  }
1590
+ const launchProfile = routingSelection?.routing?.defaultRoute;
1268
1591
  const previousScopeKey = currentScope.scopeKey;
1269
1592
  const baseConfig = buildInteractiveBaseConfig(flowId, currentScope);
1270
1593
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
@@ -1279,13 +1602,20 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1279
1602
  if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
1280
1603
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1281
1604
  }
1282
- printPanel("Effective Launch Config", `executor: ${launchProfile.executor}\nmodel: ${launchProfile.model}\nmode: ${launchMode}`, "cyan");
1605
+ if (routingSelection?.routing) {
1606
+ printPanel("Effective Launch Config", `preset: ${routingSelection.selectedPreset.label}\nmode: ${launchMode}\n${describeExecutionRouting(routingSelection.routing, routingGroups)}`, "cyan");
1607
+ }
1283
1608
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1284
- await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, launchProfile, createRuntimeServices(abortController.signal));
1609
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, launchProfile, routingSelection?.routing, routingSelection?.selectedPreset, createRuntimeServices(abortController.signal));
1285
1610
  return;
1286
1611
  }
1287
1612
  const runtimeConfig = buildRuntimeConfig(baseConfig, currentScope);
1288
- await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh, { launchProfile }), { launchProfile }, (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1613
+ const flowOverrides = {
1614
+ ...(launchProfile ? { launchProfile } : {}),
1615
+ ...(routingSelection?.routing ? { executionRouting: routingSelection.routing } : {}),
1616
+ ...(routingSelection?.selectedPreset ? { selectedRoutingPreset: routingSelection.selectedPreset } : {}),
1617
+ };
1618
+ await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh, flowOverrides), flowOverrides, (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1289
1619
  }
1290
1620
  catch (error) {
1291
1621
  if (error instanceof FlowInterruptedError) {
@@ -1363,8 +1693,8 @@ export async function main(argv = process.argv.slice(2)) {
1363
1693
  if (args.length === 1 && !args[0]?.startsWith("-") && !COMMANDS.includes(args[0])) {
1364
1694
  return await runInteractive(args[0] ?? "", forceRefresh);
1365
1695
  }
1366
- const parsedArgs = parseCliArgs(args);
1367
- const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs));
1696
+ const parsedArgs = await parseCliArgs(args);
1697
+ const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs), true, requestUserInputInTerminal, undefined, undefined, false, parsedArgs.launchMode);
1368
1698
  if (parsedArgs.command === "doctor") {
1369
1699
  return commandCompleted ? 0 : 1;
1370
1700
  }