agentweaver 0.1.9 → 0.1.11

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 (101) hide show
  1. package/README.md +226 -200
  2. package/dist/artifacts.js +101 -56
  3. package/dist/errors.js +7 -0
  4. package/dist/executors/{codex-local-executor.js → codex-executor.js} +4 -4
  5. package/dist/executors/configs/{codex-local-config.js → codex-config.js} +1 -1
  6. package/dist/executors/configs/jira-fetch-config.js +2 -0
  7. package/dist/executors/configs/telegram-notifier-config.js +3 -0
  8. package/dist/executors/fetch-gitlab-diff-executor.js +1 -1
  9. package/dist/executors/fetch-gitlab-review-executor.js +1 -1
  10. package/dist/executors/git-commit-executor.js +25 -0
  11. package/dist/executors/telegram-notifier-executor.js +54 -0
  12. package/dist/flow-state.js +46 -1
  13. package/dist/gitlab.js +13 -8
  14. package/dist/index.js +507 -520
  15. package/dist/interactive-ui.js +495 -87
  16. package/dist/jira.js +52 -5
  17. package/dist/pipeline/auto-flow.js +6 -6
  18. package/dist/pipeline/context.js +1 -0
  19. package/dist/pipeline/declarative-flows.js +7 -4
  20. package/dist/pipeline/flow-catalog.js +60 -23
  21. package/dist/pipeline/flow-model-settings.js +77 -0
  22. package/dist/pipeline/flow-specs/auto-common.json +446 -0
  23. package/dist/pipeline/flow-specs/auto-golang.json +563 -0
  24. package/dist/pipeline/flow-specs/{bug-analyze.json → bugz/bug-analyze.json} +43 -25
  25. package/dist/pipeline/flow-specs/{bug-fix.json → bugz/bug-fix.json} +5 -4
  26. package/dist/pipeline/flow-specs/git-commit.json +196 -0
  27. package/dist/pipeline/flow-specs/{gitlab-diff-review.json → gitlab/gitlab-diff-review.json} +20 -50
  28. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +165 -0
  29. package/dist/pipeline/flow-specs/{mr-description.json → gitlab/mr-description.json} +17 -10
  30. package/dist/pipeline/flow-specs/{run-go-linter-loop.json → go/run-go-linter-loop.json} +40 -14
  31. package/dist/pipeline/flow-specs/{run-go-tests-loop.json → go/run-go-tests-loop.json} +40 -14
  32. package/dist/pipeline/flow-specs/implement.json +5 -4
  33. package/dist/pipeline/flow-specs/plan.json +40 -148
  34. package/dist/pipeline/flow-specs/{review-fix.json → review/review-fix.json} +73 -13
  35. package/dist/pipeline/flow-specs/review/review-loop.json +280 -0
  36. package/dist/pipeline/flow-specs/review/review-project.json +87 -0
  37. package/dist/pipeline/flow-specs/review/review.json +126 -0
  38. package/dist/pipeline/flow-specs/task-describe.json +191 -11
  39. package/dist/pipeline/launch-profile-config.js +38 -0
  40. package/dist/pipeline/node-registry.js +75 -45
  41. package/dist/pipeline/nodes/build-failure-summary-node.js +16 -29
  42. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +36 -0
  43. package/dist/pipeline/nodes/codex-prompt-node.js +41 -0
  44. package/dist/pipeline/nodes/commit-message-form-node.js +79 -0
  45. package/dist/pipeline/nodes/git-commit-form-node.js +138 -0
  46. package/dist/pipeline/nodes/git-commit-node.js +28 -0
  47. package/dist/pipeline/nodes/git-status-node.js +221 -0
  48. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +10 -6
  49. package/dist/pipeline/nodes/jira-context-node.js +10 -0
  50. package/dist/pipeline/nodes/llm-prompt-node.js +62 -0
  51. package/dist/pipeline/nodes/plan-codex-node.js +1 -1
  52. package/dist/pipeline/nodes/read-file-node.js +11 -0
  53. package/dist/pipeline/nodes/review-findings-form-node.js +18 -14
  54. package/dist/pipeline/nodes/select-files-form-node.js +72 -0
  55. package/dist/pipeline/nodes/telegram-notifier-node.js +28 -0
  56. package/dist/pipeline/nodes/user-input-node.js +29 -8
  57. package/dist/pipeline/nodes/write-selection-file-node.js +46 -0
  58. package/dist/pipeline/prompt-registry.js +2 -4
  59. package/dist/pipeline/prompt-runtime.js +13 -3
  60. package/dist/pipeline/registry.js +6 -8
  61. package/dist/pipeline/spec-compiler.js +5 -0
  62. package/dist/pipeline/spec-loader.js +18 -7
  63. package/dist/pipeline/spec-types.js +7 -3
  64. package/dist/pipeline/spec-validator.js +4 -0
  65. package/dist/pipeline/types.js +1 -0
  66. package/dist/pipeline/value-resolver.js +40 -38
  67. package/dist/prompts.js +104 -110
  68. package/dist/runtime/agentweaver-home.js +8 -0
  69. package/dist/runtime/command-resolution.js +0 -38
  70. package/dist/runtime/env-loader.js +43 -0
  71. package/dist/runtime/process-runner.js +45 -1
  72. package/dist/structured-artifact-schema-registry.js +53 -0
  73. package/dist/structured-artifact-schemas.json +0 -20
  74. package/dist/structured-artifacts.js +3 -43
  75. package/dist/user-input.js +30 -2
  76. package/package.json +2 -6
  77. package/Dockerfile.codex +0 -56
  78. package/dist/executors/claude-executor.js +0 -46
  79. package/dist/executors/codex-docker-executor.js +0 -27
  80. package/dist/executors/configs/claude-config.js +0 -12
  81. package/dist/executors/configs/codex-docker-config.js +0 -10
  82. package/dist/executors/configs/verify-build-config.js +0 -7
  83. package/dist/executors/verify-build-executor.js +0 -123
  84. package/dist/pipeline/flow-specs/auto.json +0 -979
  85. package/dist/pipeline/flow-specs/gitlab-review.json +0 -317
  86. package/dist/pipeline/flow-specs/plan-opencode.json +0 -603
  87. package/dist/pipeline/flow-specs/preflight.json +0 -206
  88. package/dist/pipeline/flow-specs/review-project.json +0 -243
  89. package/dist/pipeline/flow-specs/review.json +0 -312
  90. package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
  91. package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
  92. package/dist/pipeline/flows/preflight-flow.js +0 -19
  93. package/dist/pipeline/nodes/claude-prompt-node.js +0 -54
  94. package/dist/pipeline/nodes/codex-docker-prompt-node.js +0 -32
  95. package/dist/pipeline/nodes/codex-local-prompt-node.js +0 -32
  96. package/dist/pipeline/nodes/review-claude-node.js +0 -38
  97. package/dist/pipeline/nodes/review-reply-codex-node.js +0 -40
  98. package/dist/pipeline/nodes/verify-build-node.js +0 -15
  99. package/dist/runtime/docker-runtime.js +0 -51
  100. package/docker-compose.yml +0 -445
  101. package/verify_build.sh +0 -105
package/dist/index.js CHANGED
@@ -1,29 +1,37 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readdirSync, 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, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewFile, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
7
- import { TaskRunnerError } from "./errors.js";
8
- import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
6
+ import { REVIEW_FILE_RE, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, latestArtifactIteration, nextArtifactIteration, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, flowStateFile, taskSummaryFile, } from "./artifacts.js";
7
+ import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
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
11
  import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
12
+ import { runNodeChecks } from "./pipeline/checks.js";
12
13
  import { createPipelineContext } from "./pipeline/context.js";
13
- import { loadAutoFlow } from "./pipeline/auto-flow.js";
14
14
  import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
15
- import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
15
+ import { runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
16
16
  import { findCatalogEntry, isBuiltInCommandFlowId, loadInteractiveFlowCatalog, toDeclarativeFlowRef } from "./pipeline/flow-catalog.js";
17
- import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
18
- import { agentweaverHome, defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
17
+ import { ALLOWED_MODELS_BY_EXECUTOR, defaultModelForExecutor, DEFAULT_LAUNCH_PROFILE, LLM_EXECUTOR_IDS, resolveLaunchProfile, } from "./pipeline/launch-profile-config.js";
18
+ import { evaluateCondition, resolveValue } from "./pipeline/value-resolver.js";
19
+ import { resolveCmd } from "./runtime/command-resolution.js";
20
+ import { loadTieredEnv } from "./runtime/env-loader.js";
21
+ import { agentweaverHome } from "./runtime/agentweaver-home.js";
19
22
  import { runCommand } from "./runtime/process-runner.js";
20
23
  import { InteractiveUi } from "./interactive-ui.js";
21
24
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
22
25
  import { requestUserInputInTerminal } from "./user-input.js";
23
26
  import { attachJiraContext, detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
24
27
  const COMMANDS = [
28
+ "auto-golang",
29
+ "auto-common",
30
+ "auto-status",
31
+ "auto-reset",
25
32
  "bug-analyze",
26
33
  "bug-fix",
34
+ "git-commit",
27
35
  "gitlab-diff-review",
28
36
  "gitlab-review",
29
37
  "mr-description",
@@ -32,21 +40,19 @@ const COMMANDS = [
32
40
  "implement",
33
41
  "review",
34
42
  "review-fix",
43
+ "review-loop",
35
44
  "run-go-tests-loop",
36
45
  "run-go-linter-loop",
37
- "auto",
38
- "auto-status",
39
- "auto-reset",
40
46
  ];
41
- const AUTO_STATE_SCHEMA_VERSION = 3;
42
47
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
43
48
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
44
- const runtimeServices = {
45
- resolveCmd,
46
- resolveDockerComposeCmd,
47
- dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
48
- runCommand,
49
- };
49
+ function createRuntimeServices(signal) {
50
+ return {
51
+ resolveCmd,
52
+ runCommand: (argv, options = {}) => runCommand(argv, { ...options, ...(signal ? { signal } : {}) }),
53
+ };
54
+ }
55
+ const runtimeServices = createRuntimeServices();
50
56
  function buildFailureOutputPreview(output) {
51
57
  const normalized = stripAnsi(output).replace(/\r\n/g, "\n").trim();
52
58
  if (!normalized) {
@@ -78,59 +84,64 @@ function formatProcessFailure(error) {
78
84
  }
79
85
  return `${baseMessage}\nПричина:\n${preview}`;
80
86
  }
87
+ function escapeRegExp(value) {
88
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
89
+ }
81
90
  function usage() {
82
91
  return `Usage:
83
92
  agentweaver
84
93
  agentweaver <jira-browse-url|jira-issue-key>
85
94
  agentweaver --force <jira-browse-url|jira-issue-key>
95
+ agentweaver git-commit [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
86
96
  agentweaver gitlab-diff-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
87
97
  agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
88
98
  agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
89
99
  agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
90
100
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
91
- agentweaver plan [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
92
- agentweaver task-describe [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
101
+ agentweaver plan [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [<jira-browse-url|jira-issue-key>]
102
+ agentweaver task-describe [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
93
103
  agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
94
104
  agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
95
105
  agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
106
+ agentweaver review-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
96
107
  agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
97
108
  agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
98
- agentweaver auto [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
99
- agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
100
- agentweaver auto --help-phases
109
+ agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
110
+ agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
111
+ agentweaver auto-golang --help-phases
101
112
  agentweaver auto-status [<jira-browse-url|jira-issue-key>]
102
113
  agentweaver auto-reset [<jira-browse-url|jira-issue-key>]
103
114
 
104
115
  Interactive Mode:
105
116
  When started without a command, the script opens an interactive UI.
106
117
  If a Jira task is provided, interactive mode starts in the current project scope with Jira context attached.
107
- Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
118
+ Use Up/Down to move in the flow tree, Left/Right to collapse or expand folders, Enter to toggle a folder or run a flow, h for help, q to exit.
108
119
 
109
120
  Flags:
110
121
  --version Show package version
111
122
  --force In interactive mode, regenerate task summary in Jira-backed flows
112
- --dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
123
+ --dry Fetch Jira task, but print codex/opencode commands instead of executing them
113
124
  --verbose Show live stdout/stderr of launched commands
114
125
  --scope Explicit workflow scope name for non-Jira runs
115
126
  --prompt Extra prompt text appended to the base prompt
127
+ --md-lang Language for markdown output files: en (English) or ru (Russian, default)
116
128
 
117
129
  Required environment variables:
118
- JIRA_API_KEY Jira API key used in Authorization: Bearer <token> for Jira-backed flows
130
+ JIRA_API_KEY Jira API token used for Jira-backed flows (Bearer by default, or Basic with Jira Cloud)
119
131
 
120
132
  Optional environment variables:
133
+ JIRA_USERNAME Required for Jira Cloud Basic auth (usually Atlassian account email)
134
+ JIRA_AUTH_MODE Override Jira auth mode: auto | basic | bearer
121
135
  JIRA_BASE_URL
122
136
  GITLAB_TOKEN
123
137
  AGENTWEAVER_HOME
124
- DOCKER_COMPOSE_BIN
125
138
  CODEX_BIN
126
139
  CODEX_MODEL
127
140
  OPENCODE_BIN
128
141
  OPENCODE_MODEL
129
- CLAUDE_BIN
130
- CLAUDE_MODEL
131
142
 
132
143
  Notes:
133
- - Task-only flows will ask for Jira task via user-input when it is not passed as an argument.
144
+ - 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.
134
145
  - All flow state and artifacts are stored in the current project scope by default.
135
146
  - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.`;
136
147
  }
@@ -142,212 +153,235 @@ function packageVersion() {
142
153
  }
143
154
  return raw.version;
144
155
  }
145
- function nowIso8601() {
146
- return new Date().toISOString();
147
- }
148
156
  function normalizeAutoPhaseId(phaseId) {
149
157
  return phaseId.trim().toLowerCase().replaceAll("-", "_");
150
158
  }
151
- function buildAutoSteps() {
152
- return loadAutoFlow().phases.map((phase) => ({
153
- id: phase.id,
154
- status: "pending",
155
- }));
156
- }
157
159
  function autoPhaseIds() {
158
- return buildAutoSteps().map((step) => step.id);
160
+ return loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases.map((phase) => phase.id);
159
161
  }
160
162
  function validateAutoPhaseId(phaseId) {
161
163
  const normalized = normalizeAutoPhaseId(phaseId);
162
164
  if (!autoPhaseIds().includes(normalized)) {
163
- throw new TaskRunnerError(`Unknown auto phase: ${phaseId}\nUse 'agentweaver auto --help-phases' or '/help auto' to list valid phases.`);
165
+ throw new TaskRunnerError(`Unknown auto-golang phase: ${phaseId}\nUse 'agentweaver auto-golang --help-phases' or '/help auto-golang' to list valid phases.`);
164
166
  }
165
167
  return normalized;
166
168
  }
167
- function createAutoPipelineState(config) {
168
- const autoFlow = loadAutoFlow();
169
- const maxReviewIterations = autoFlow.phases.filter((phase) => /^review_\d+$/.test(phase.id)).length;
170
- return {
171
- schemaVersion: AUTO_STATE_SCHEMA_VERSION,
172
- issueKey: config.taskKey,
173
- jiraRef: config.jiraRef,
174
- status: "pending",
175
- currentStep: null,
176
- maxReviewIterations,
177
- updatedAt: nowIso8601(),
178
- steps: buildAutoSteps(),
179
- executionState: {
180
- flowKind: autoFlow.kind,
181
- flowVersion: autoFlow.version,
182
- terminated: false,
183
- phases: [],
184
- },
185
- };
186
- }
187
- function loadAutoPipelineState(config) {
188
- const filePath = autoStateFile(config.taskKey);
189
- if (!existsSync(filePath)) {
190
- return null;
191
- }
192
- let raw;
193
- try {
194
- raw = JSON.parse(readFileSync(filePath, "utf8"));
169
+ function buildFlowResumeDetails(state) {
170
+ const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
171
+ const lines = [
172
+ "Interrupted run found.",
173
+ `Current step: ${currentStep}`,
174
+ `Updated: ${state.updatedAt}`,
175
+ ];
176
+ if (state.launchProfile) {
177
+ lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
195
178
  }
196
- catch (error) {
197
- throw new TaskRunnerError(`Failed to parse auto state file ${filePath}: ${error.message}`);
198
- }
199
- if (!raw || typeof raw !== "object") {
200
- throw new TaskRunnerError(`Invalid auto state file format: ${filePath}`);
201
- }
202
- const state = raw;
203
- if (state.schemaVersion !== AUTO_STATE_SCHEMA_VERSION) {
204
- throw new TaskRunnerError(`Unsupported auto state schema in ${filePath}: ${state.schemaVersion}`);
205
- }
206
- if (!state.executionState) {
207
- const autoFlow = loadAutoFlow();
208
- state.executionState = {
209
- flowKind: autoFlow.kind,
210
- flowVersion: autoFlow.version,
211
- terminated: false,
212
- phases: [],
213
- };
179
+ if (state.lastError) {
180
+ lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
214
181
  }
215
- syncAutoStepsFromExecutionState(state);
216
- return state;
182
+ return lines.join("\n");
217
183
  }
218
- function saveAutoPipelineState(state) {
219
- state.updatedAt = nowIso8601();
220
- ensureScopeWorkspaceDir(state.issueKey);
221
- writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify({
222
- ...state,
223
- executionState: stripExecutionStatePayload(state.executionState),
224
- }, null, 2)}\n`, "utf8");
184
+ function launchProfileSelectionForm() {
185
+ const defaultExecutor = DEFAULT_LAUNCH_PROFILE.executor;
186
+ return {
187
+ formId: "flow-launch-profile",
188
+ title: "Настройки запуска LLM",
189
+ description: `Выберите executor для запуска flow. Текущий default: ${defaultExecutor}.`,
190
+ submitLabel: "Continue",
191
+ fields: [
192
+ {
193
+ id: "executor",
194
+ type: "single-select",
195
+ label: "Executor",
196
+ required: true,
197
+ default: "default",
198
+ options: [
199
+ { value: "default", label: `default (${defaultExecutor})` },
200
+ ...LLM_EXECUTOR_IDS.map((id) => ({
201
+ value: id,
202
+ label: id === defaultExecutor ? `${id} [default]` : id,
203
+ })),
204
+ ],
205
+ },
206
+ ],
207
+ };
225
208
  }
226
- function syncAndSaveAutoPipelineState(state) {
227
- syncAutoStepsFromExecutionState(state);
228
- saveAutoPipelineState(state);
209
+ function launchModelSelectionForm(executor) {
210
+ const resolvedExecutor = executor === "default" ? DEFAULT_LAUNCH_PROFILE.executor : executor;
211
+ const defaultModel = defaultModelForExecutor(resolvedExecutor);
212
+ const options = executor === "default"
213
+ ? [{ value: "default", label: `default (${DEFAULT_LAUNCH_PROFILE.model})` }]
214
+ : [
215
+ { value: "default", label: `default (${defaultModel})` },
216
+ ...ALLOWED_MODELS_BY_EXECUTOR[executor].map((model) => ({
217
+ value: model,
218
+ label: model === defaultModel ? `${model} [default]` : model,
219
+ })),
220
+ ];
221
+ return {
222
+ formId: "flow-launch-model",
223
+ title: "Настройки запуска LLM",
224
+ description: `Выберите модель для запуска flow. Текущий default для ${resolvedExecutor}: ${defaultModel}.`,
225
+ submitLabel: "Start",
226
+ fields: [
227
+ {
228
+ id: "model",
229
+ type: "single-select",
230
+ label: "Model",
231
+ required: true,
232
+ default: "default",
233
+ options,
234
+ },
235
+ ],
236
+ };
229
237
  }
230
- function resetAutoPipelineState(config) {
231
- const filePath = autoStateFile(config.taskKey);
232
- if (!existsSync(filePath)) {
233
- return false;
234
- }
235
- rmSync(filePath);
236
- return true;
238
+ async function requestInteractiveLaunchProfile(requestUserInput) {
239
+ const executorFormResult = await requestUserInput(launchProfileSelectionForm());
240
+ const rawExecutor = String(executorFormResult.values.executor ?? "default");
241
+ const executor = rawExecutor === "default" ? "default" : LLM_EXECUTOR_IDS.find((id) => id === rawExecutor);
242
+ if (!executor) {
243
+ throw new TaskRunnerError(`Unsupported launch executor '${rawExecutor}'.`);
244
+ }
245
+ const modelFormResult = await requestUserInput(launchModelSelectionForm(executor));
246
+ const rawModel = String(modelFormResult.values.model ?? "default").trim();
247
+ return resolveLaunchProfile({
248
+ executor,
249
+ model: rawModel.length > 0 ? rawModel : "default",
250
+ }, DEFAULT_LAUNCH_PROFILE);
237
251
  }
238
- function nextAutoStep(state) {
239
- return state.steps.find((step) => ["running", "failed", "pending"].includes(step.status)) ?? null;
252
+ function buildResolverContext(pipelineContext, flowParams, flowConstants, repeatVars, executionState) {
253
+ return {
254
+ flowParams,
255
+ flowConstants,
256
+ pipelineContext,
257
+ repeatVars,
258
+ executionState,
259
+ };
240
260
  }
241
- function findCurrentExecutionStep(state) {
242
- for (const phase of state.executionState.phases) {
243
- const runningStep = phase.steps.find((step) => step.status === "running");
244
- if (runningStep) {
245
- return `${phase.id}:${runningStep.id}`;
261
+ function resolveResumeChecks(step, context) {
262
+ return (step.expect ?? [])
263
+ .filter((expectation) => evaluateCondition(expectation.when, context))
264
+ .flatMap((expectation) => {
265
+ if (expectation.kind === "step-output") {
266
+ const value = resolveValue(expectation.value, context);
267
+ if (expectation.equals !== undefined) {
268
+ const expected = resolveValue(expectation.equals, context);
269
+ if (value !== expected) {
270
+ throw new TaskRunnerError(expectation.message);
271
+ }
272
+ return [];
273
+ }
274
+ if (!value) {
275
+ throw new TaskRunnerError(expectation.message);
276
+ }
277
+ return [];
246
278
  }
247
- }
248
- return null;
279
+ if (expectation.kind === "require-artifacts") {
280
+ const value = resolveValue(expectation.paths, context);
281
+ if (!Array.isArray(value) || value.some((candidate) => typeof candidate !== "string")) {
282
+ throw new TaskRunnerError("Expectation 'require-artifacts' must resolve to string[]");
283
+ }
284
+ return [{ kind: "require-artifacts", paths: value, message: expectation.message }];
285
+ }
286
+ if (expectation.kind === "require-file") {
287
+ const value = resolveValue(expectation.path, context);
288
+ if (typeof value !== "string") {
289
+ throw new TaskRunnerError("Expectation 'require-file' must resolve to string");
290
+ }
291
+ return [{ kind: "require-file", path: value, message: expectation.message }];
292
+ }
293
+ const items = expectation.items.map((item) => {
294
+ const value = resolveValue(item.path, context);
295
+ if (typeof value !== "string") {
296
+ throw new TaskRunnerError("Expectation 'require-structured-artifacts' item path must resolve to string");
297
+ }
298
+ return {
299
+ path: value,
300
+ schemaId: item.schemaId,
301
+ };
302
+ });
303
+ return [{ kind: "require-structured-artifacts", items, message: expectation.message }];
304
+ });
249
305
  }
250
- function deriveAutoPipelineStatus(state) {
251
- if (state.lastError || state.steps.some((candidate) => candidate.status === "failed")) {
252
- return "blocked";
253
- }
254
- if (state.executionState.terminated) {
255
- return "completed";
256
- }
257
- if (state.steps.some((candidate) => candidate.status === "running")) {
258
- return "running";
259
- }
260
- if (state.steps.some((candidate) => candidate.status === "pending")) {
261
- return "pending";
262
- }
263
- if (state.steps.some((candidate) => candidate.status === "skipped")) {
264
- return "completed";
306
+ function validateDeclarativePhaseResumeState(phase, phaseState, pipelineContext, flowParams, flowConstants, executionState) {
307
+ if (phaseState.status === "done") {
308
+ return;
265
309
  }
266
- if (state.steps.every((candidate) => candidate.status === "done")) {
267
- return "completed";
310
+ for (const [stepIndex, step] of phase.steps.entries()) {
311
+ const stepState = phaseState.steps[stepIndex];
312
+ if (!stepState || stepState.status !== "done") {
313
+ continue;
314
+ }
315
+ const context = buildResolverContext(pipelineContext, flowParams, flowConstants, step.repeatVars, executionState);
316
+ const checks = resolveResumeChecks(step, context);
317
+ try {
318
+ runNodeChecks(checks);
319
+ }
320
+ catch (error) {
321
+ throw new TaskRunnerError(`Resume is impossible for '${phase.id}:${step.id}' because required artifacts are missing or invalid. Use restart.\n${error.message}`);
322
+ }
268
323
  }
269
- return state.status;
270
324
  }
271
- function printAutoState(state) {
272
- const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
273
- const lines = [
274
- `Issue: ${state.issueKey}`,
275
- `Status: ${deriveAutoPipelineStatus(state)}`,
276
- `Current step: ${currentStep}`,
277
- `Updated: ${state.updatedAt}`,
278
- ];
279
- if (state.lastError) {
280
- lines.push(`Last error: ${state.lastError.step ?? "-"} (exit ${state.lastError.returnCode ?? "-"}, ${state.lastError.message ?? "-"})`);
281
- }
282
- lines.push("");
283
- for (const step of state.steps) {
284
- lines.push(`[${step.status}] ${step.id}${step.note ? ` (${step.note})` : ""}`);
285
- const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
286
- for (const childStep of phaseState?.steps ?? []) {
287
- lines.push(` - [${childStep.status}] ${childStep.id}`);
325
+ function validateDeclarativeFlowResumeState(flowEntry, config, state, launchProfile, runtime = runtimeServices) {
326
+ if (state.launchProfile) {
327
+ if (!launchProfile) {
328
+ throw new TaskRunnerError("Resume is impossible because launch profile is missing. Use restart.");
329
+ }
330
+ if (state.launchProfile.fingerprint !== launchProfile.fingerprint) {
331
+ throw new TaskRunnerError(`Resume is impossible because launch profile changed (${state.launchProfile.executor}/${state.launchProfile.model} -> ${launchProfile.executor}/${launchProfile.model}). Use restart.`);
288
332
  }
289
333
  }
290
- if (state.executionState.terminated) {
291
- lines.push("", `Execution terminated: ${state.executionState.terminationReason ?? "yes"}`);
334
+ if (flowRequiresTaskScope(flowEntry) && !config.jiraRef) {
335
+ throw new TaskRunnerError("Resume is impossible because Jira context is missing for this flow state. Use restart.");
292
336
  }
293
- printPanel("Auto Status", lines.join("\n"), "cyan");
294
- }
295
- function syncAutoStepsFromExecutionState(state) {
296
- for (const step of state.steps) {
297
- const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
337
+ const pipelineContext = createPipelineContext({
338
+ issueKey: config.taskKey,
339
+ jiraRef: config.jiraRef,
340
+ dryRun: config.dryRun,
341
+ verbose: config.verbose,
342
+ ...(config.mdLang !== undefined ? { mdLang: config.mdLang } : {}),
343
+ runtime,
344
+ requestUserInput: requestUserInputInTerminal,
345
+ });
346
+ const flowParams = defaultDeclarativeFlowParams(config, false, launchProfile ? { launchProfile } : {});
347
+ for (const phase of flowEntry.flow.phases) {
348
+ const phaseState = state.executionState.phases.find((candidate) => candidate.id === phase.id);
298
349
  if (!phaseState) {
299
350
  continue;
300
351
  }
301
- step.status = phaseState.status;
302
- step.startedAt = phaseState.startedAt ?? null;
303
- step.finishedAt = phaseState.finishedAt ?? null;
304
- step.note = null;
305
- if (phaseState.status === "skipped") {
306
- step.note = "condition not met";
307
- step.returnCode ??= 0;
308
- }
309
- else if (phaseState.status === "done") {
310
- step.returnCode ??= 0;
311
- if (state.executionState.terminated && state.executionState.terminationReason?.startsWith(`Stopped by ${step.id}:`)) {
312
- step.note = "stop condition met";
313
- }
314
- }
315
- }
316
- state.currentStep = findCurrentExecutionStep(state);
317
- state.status = deriveAutoPipelineStatus(state);
318
- }
319
- function buildAutoResumeDetails(state) {
320
- const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
321
- const lines = [
322
- "Interrupted auto run found.",
323
- `Current step: ${currentStep}`,
324
- `Updated: ${state.updatedAt}`,
325
- ];
326
- if (state.lastError) {
327
- lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
352
+ validateDeclarativePhaseResumeState(phase, phaseState, pipelineContext, flowParams, flowEntry.flow.constants, state.executionState);
328
353
  }
329
- return lines.join("\n");
330
354
  }
331
- function buildFlowResumeDetails(state) {
332
- const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
333
- const lines = [
334
- "Interrupted run found.",
335
- `Current step: ${currentStep}`,
336
- `Updated: ${state.updatedAt}`,
337
- ];
338
- if (state.lastError) {
339
- lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
355
+ function scopeWithRestoredJiraContext(scope, state) {
356
+ if (scope.jiraRef || !state?.jiraRef?.trim()) {
357
+ return scope;
340
358
  }
341
- return lines.join("\n");
359
+ return attachJiraContext(scope, state.jiraRef);
342
360
  }
343
361
  function lookupInteractiveFlowResume(flowEntry, currentScope) {
344
362
  const directState = loadFlowRunState(currentScope.scopeKey, flowEntry.id);
345
363
  if (directState && hasResumableFlowState(directState)) {
346
- return {
347
- resumeAvailable: true,
348
- hasExistingState: true,
349
- details: buildFlowResumeDetails(directState),
350
- };
364
+ try {
365
+ const effectiveScope = scopeWithRestoredJiraContext(currentScope, directState);
366
+ const baseConfig = buildBaseConfig(flowEntry.id, {
367
+ ...(effectiveScope.jiraRef ? { jiraRef: effectiveScope.jiraRef } : {}),
368
+ scopeName: effectiveScope.scopeKey,
369
+ });
370
+ const config = buildRuntimeConfig(baseConfig, effectiveScope);
371
+ validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.launchProfile);
372
+ return {
373
+ resumeAvailable: true,
374
+ hasExistingState: true,
375
+ details: buildFlowResumeDetails(directState),
376
+ };
377
+ }
378
+ catch (error) {
379
+ return {
380
+ resumeAvailable: false,
381
+ hasExistingState: true,
382
+ details: `Interrupted run found, but resume is unavailable.\n${error.message}`,
383
+ };
384
+ }
351
385
  }
352
386
  return {
353
387
  resumeAvailable: false,
@@ -355,37 +389,17 @@ function lookupInteractiveFlowResume(flowEntry, currentScope) {
355
389
  };
356
390
  }
357
391
  function printAutoPhasesHelp() {
358
- const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
359
- phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
360
- printPanel("Auto Phases", phaseLines.join("\n"), "magenta");
392
+ const phaseLines = ["Available auto-golang phases:", "", ...autoPhaseIds()];
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>");
394
+ printPanel("Auto-Golang Phases", phaseLines.join("\n"), "magenta");
361
395
  }
362
- function loadEnvFile(envFilePath) {
363
- if (!existsSync(envFilePath)) {
364
- return;
365
- }
366
- const lines = readFileSync(envFilePath, "utf8").split(/\r?\n/);
367
- for (const rawLine of lines) {
368
- let line = rawLine.trim();
369
- if (!line || line.startsWith("#")) {
370
- continue;
371
- }
372
- if (line.startsWith("export ")) {
373
- line = line.slice(7).trim();
374
- }
375
- const separatorIndex = line.indexOf("=");
376
- if (separatorIndex < 0) {
377
- continue;
378
- }
379
- const key = line.slice(0, separatorIndex).trim();
380
- if (!key || process.env[key] !== undefined) {
381
- continue;
382
- }
383
- let value = line.slice(separatorIndex + 1).trim();
384
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
385
- value = value.slice(1, -1);
386
- }
387
- process.env[key] = value;
388
- }
396
+ function autoCommonPhaseIds() {
397
+ return loadDeclarativeFlow({ source: "built-in", fileName: "auto-common.json" }).phases.map((phase) => phase.id);
398
+ }
399
+ function printAutoCommonPhasesHelp() {
400
+ const phaseLines = ["Available auto-common phases:", "", ...autoCommonPhaseIds()];
401
+ phaseLines.push("", "You can run auto-common with:", "agentweaver auto-common <jira>");
402
+ printPanel("Auto-Common Phases", phaseLines.join("\n"), "magenta");
389
403
  }
390
404
  function nextReviewIterationForTask(taskKey) {
391
405
  let maxIndex = 0;
@@ -397,33 +411,14 @@ function nextReviewIterationForTask(taskKey) {
397
411
  if (!entry.isFile()) {
398
412
  continue;
399
413
  }
400
- const match = REVIEW_FILE_RE.exec(entry.name) ?? REVIEW_REPLY_FILE_RE.exec(entry.name);
414
+ const match = REVIEW_FILE_RE.exec(entry.name);
401
415
  if (match && match[1] === taskKey) {
402
416
  maxIndex = Math.max(maxIndex, Number.parseInt(match[2] ?? "0", 10));
403
417
  }
404
418
  }
405
419
  return maxIndex + 1;
406
420
  }
407
- function latestReviewReplyIteration(taskKey) {
408
- let maxIndex = null;
409
- const workspaceDir = scopeWorkspaceDir(taskKey);
410
- if (!existsSync(workspaceDir)) {
411
- return null;
412
- }
413
- for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
414
- if (!entry.isFile()) {
415
- continue;
416
- }
417
- const match = REVIEW_REPLY_FILE_RE.exec(entry.name);
418
- if (match && match[1] === taskKey) {
419
- const current = Number.parseInt(match[2] ?? "0", 10);
420
- maxIndex = maxIndex === null ? current : Math.max(maxIndex, current);
421
- }
422
- }
423
- return maxIndex;
424
- }
425
421
  function buildBaseConfig(command, options = {}) {
426
- const homeDir = agentweaverHome(PACKAGE_ROOT);
427
422
  return {
428
423
  command,
429
424
  jiraRef: options.jiraRef ?? null,
@@ -431,12 +426,9 @@ function buildBaseConfig(command, options = {}) {
431
426
  reviewFixPoints: options.reviewFixPoints ?? null,
432
427
  extraPrompt: options.extraPrompt ?? null,
433
428
  autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
429
+ mdLang: options.mdLang ?? null,
434
430
  dryRun: options.dryRun ?? false,
435
431
  verbose: options.verbose ?? false,
436
- dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
437
- runGoTestsScript: path.join(homeDir, "run_go_tests.py"),
438
- runGoLinterScript: path.join(homeDir, "run_go_linter.py"),
439
- runGoCoverageScript: path.join(homeDir, "run_go_coverage.sh"),
440
432
  };
441
433
  }
442
434
  function commandRequiresTask(command) {
@@ -444,17 +436,20 @@ function commandRequiresTask(command) {
444
436
  command === "bug-analyze" ||
445
437
  command === "bug-fix" ||
446
438
  command === "mr-description" ||
447
- command === "task-describe" ||
448
- command === "auto" ||
439
+ command === "auto-golang" ||
440
+ command === "auto-common" ||
449
441
  command === "auto-status" ||
450
442
  command === "auto-reset");
451
443
  }
452
444
  function commandSupportsProjectScope(command) {
453
- return (command === "gitlab-diff-review" ||
445
+ return (command === "git-commit" ||
446
+ command === "gitlab-diff-review" ||
454
447
  command === "gitlab-review" ||
448
+ command === "task-describe" ||
455
449
  command === "implement" ||
456
450
  command === "review" ||
457
451
  command === "review-fix" ||
452
+ command === "review-loop" ||
458
453
  command === "run-go-tests-loop" ||
459
454
  command === "run-go-linter-loop");
460
455
  }
@@ -503,41 +498,40 @@ function checkPrerequisites(config) {
503
498
  config.command === "run-go-linter-loop") {
504
499
  resolveCmd("codex", "CODEX_BIN");
505
500
  }
506
- if (config.command === "review" || config.command === "gitlab-diff-review") {
507
- resolveCmd("claude", "CLAUDE_BIN");
508
- }
509
501
  }
510
502
  function checkAutoPrerequisites(config) {
511
503
  resolveCmd("codex", "CODEX_BIN");
512
- resolveCmd("claude", "CLAUDE_BIN");
513
504
  }
514
505
  function autoFlowParams(config, forceRefreshSummary = false) {
515
506
  return {
516
507
  jiraApiUrl: config.jiraApiUrl,
517
508
  taskKey: config.taskKey,
518
- dockerComposeFile: config.dockerComposeFile,
519
- runGoTestsScript: config.runGoTestsScript,
520
- runGoLinterScript: config.runGoLinterScript,
521
- runGoCoverageScript: config.runGoCoverageScript,
522
509
  extraPrompt: config.extraPrompt,
523
510
  reviewFixPoints: config.reviewFixPoints,
524
511
  forceRefresh: forceRefreshSummary,
512
+ mdLang: config.mdLang,
513
+ runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
514
+ runGoLinterScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_linter.py"),
515
+ runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
516
+ runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
525
517
  };
526
518
  }
527
519
  const FLOW_DESCRIPTIONS = {
528
- auto: "Полный пайплайн задачи: планирование, реализация, проверки, ревью, ответы на ревью и повторные итерации до готовности к merge.",
529
- "bug-analyze": "Анализирует баг по Jira и создаёт структурированные артефакты: гипотезу причины, дизайн исправления и план работ.",
530
- "gitlab-diff-review": "Запрашивает GitLab MR URL через user-input, загружает diff merge request по API и запускает код-ревью через Claude Opus с сохранением markdown и structured JSON artifacts.",
531
- "gitlab-review": "Запрашивает GitLab MR URL через user-input, загружает комментарии код-ревью по API и сохраняет markdown плюс structured JSON artifact.",
532
- "bug-fix": "Берёт результаты bug-analyze как source of truth и реализует исправление бага в коде.",
533
- "mr-description": "Готовит краткое intent-описание для merge request на основе задачи и текущих изменений.",
534
- plan: "Загружает задачу из Jira и создаёт дизайн, план реализации и QA-план в structured JSON и markdown.",
535
- "task-describe": "Строит короткое резюме задачи на основе Jira-артефакта для быстрого ознакомления.",
536
- implement: "Реализует задачу по утверждённым design/plan артефактам и при необходимости запускает post-verify сборки.",
537
- review: "Запускает Claude-код-ревью текущих изменений, валидирует structured findings, затем готовит ответ на замечания через Codex.",
538
- "review-fix": "Исправляет замечания после review-reply, обновляет код и прогоняет обязательные проверки после правок.",
539
- "run-go-tests-loop": "Циклически запускает `./run_go_tests.py` локально, анализирует последнюю ошибку и правит код до успешного прохождения или исчерпания попыток.",
540
- "run-go-linter-loop": "Циклически запускает `./run_go_linter.py` локально, исправляет проблемы линтера или генерации и повторяет попытки до успеха.",
520
+ "auto-golang": "Full task pipeline: planning, implementation, checks, review, review replies, and repeated iterations until ready to merge.",
521
+ "bug-analyze": "Analyzes bug from Jira and creates structured artifacts: root cause hypothesis, fix design, and implementation plan.",
522
+ "git-commit": "Collects git status/diff, generates commit message via LLM, allows file selection and commit confirmation.",
523
+ "gitlab-diff-review": "Requests GitLab MR URL via user-input, downloads merge request diff via API, and runs code review with markdown and structured JSON artifacts.",
524
+ "gitlab-review": "Requests GitLab MR URL via user-input, downloads code review comments via API, and saves markdown plus structured JSON artifact.",
525
+ "bug-fix": "Takes bug-analyze results as source of truth and implements the bug fix in code.",
526
+ "mr-description": "Prepares a brief intent description for a merge request based on the task and current changes.",
527
+ plan: "Loads task from Jira and creates design, implementation plan, and QA plan in structured JSON and markdown.",
528
+ "task-describe": "Builds a brief task description either from Jira or from quick user-input without Jira.",
529
+ implement: "Implements the task from approved design/plan artifacts and runs post-verify builds if needed.",
530
+ review: "Runs code review of current changes, validates structured findings, then prepares a reply to comments.",
531
+ "review-fix": "Fixes issues after review-reply, updates code, and runs mandatory checks after modifications.",
532
+ "review-loop": "Iteratively runs review and review-fix cycles up to 5 times until ready-to-merge is achieved.",
533
+ "run-go-tests-loop": "Cycles through `./run_go_tests.py` locally, analyzes the last error, and fixes code until successful or attempts exhausted.",
534
+ "run-go-linter-loop": "Cycles through `./run_go_linter.py` locally, fixes linter or generation issues, and retries until success.",
541
535
  };
542
536
  function flowDescription(id) {
543
537
  return FLOW_DESCRIPTIONS[id] ?? "Описание для этого flow пока не задано.";
@@ -549,6 +543,7 @@ function interactiveFlowDefinition(entry) {
549
543
  label: entry.id,
550
544
  description: flowDescription(entry.id),
551
545
  source: entry.source,
546
+ treePath: [...entry.treePath],
552
547
  ...(entry.source === "project-local" ? { sourcePath: entry.absolutePath } : {}),
553
548
  phases: flow.phases.map((phase) => ({
554
549
  id: phase.id,
@@ -598,13 +593,14 @@ function findCurrentFlowExecutionStep(state) {
598
593
  }
599
594
  return null;
600
595
  }
601
- async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
596
+ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, overrides = {}, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
602
597
  const context = createPipelineContext({
603
598
  issueKey: config.taskKey,
604
599
  jiraRef: config.jiraRef,
605
600
  dryRun: config.dryRun,
606
601
  verbose: config.verbose,
607
- runtime: runtimeServices,
602
+ ...(config.mdLang !== undefined ? { mdLang: config.mdLang } : {}),
603
+ runtime,
608
604
  ...(setSummary ? { setSummary } : {}),
609
605
  requestUserInput,
610
606
  });
@@ -617,13 +613,25 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requ
617
613
  };
618
614
  let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
619
615
  if (persistedState && launchMode === "resume") {
616
+ validateDeclarativeFlowResumeState({
617
+ id: flowId,
618
+ source: flow.source,
619
+ fileName: flow.fileName,
620
+ absolutePath: flow.absolutePath,
621
+ treePath: [],
622
+ flow,
623
+ }, config, persistedState, overrides.launchProfile, runtime);
620
624
  persistedState = prepareFlowStateForResume(persistedState);
621
625
  }
622
626
  else if (launchMode === "restart") {
623
627
  resetFlowRunState(config.scope.scopeKey, flowId);
624
628
  }
625
629
  const executionState = persistedState?.executionState ?? initialExecutionState;
626
- const state = persistedState ?? createFlowRunState(config.scope.scopeKey, flowId, executionState);
630
+ const state = persistedState
631
+ ?? createFlowRunState(config.scope.scopeKey, flowId, executionState, config.jiraRef, overrides.launchProfile);
632
+ if (overrides.launchProfile) {
633
+ state.launchProfile = overrides.launchProfile;
634
+ }
627
635
  state.status = "running";
628
636
  state.lastError = null;
629
637
  state.currentStep = findCurrentFlowExecutionStep(state);
@@ -669,10 +677,17 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requ
669
677
  throw error;
670
678
  }
671
679
  }
672
- async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
673
- await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, flowParams, requestUserInput, setSummary, launchMode);
680
+ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, overrides = {}, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
681
+ const mergedFlowParams = {
682
+ ...defaultDeclarativeFlowParams(config, false, overrides),
683
+ ...flowParams,
684
+ };
685
+ await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, mergedFlowParams, overrides, requestUserInput, setSummary, launchMode, runtime);
674
686
  }
675
- function defaultDeclarativeFlowParams(config, forceRefreshSummary = false) {
687
+ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overrides = {}) {
688
+ const iteration = nextReviewIterationForTask(config.taskKey);
689
+ const latestIteration = latestArtifactIteration(config.taskKey, "review");
690
+ const launchProfile = overrides.launchProfile ?? resolveLaunchProfile({ executor: "default", model: "default" }, DEFAULT_LAUNCH_PROFILE);
676
691
  return {
677
692
  taskKey: config.taskKey,
678
693
  jiraRef: config.jiraRef,
@@ -680,12 +695,20 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false) {
680
695
  jiraApiUrl: config.jiraApiUrl,
681
696
  jiraTaskFile: config.jiraTaskFile,
682
697
  scopeKey: config.scope.scopeKey,
683
- dockerComposeFile: config.dockerComposeFile,
684
- runGoTestsScript: config.runGoTestsScript,
685
- runGoLinterScript: config.runGoLinterScript,
686
- runGoCoverageScript: config.runGoCoverageScript,
698
+ workspaceDir: scopeWorkspaceDir(config.taskKey),
687
699
  extraPrompt: config.extraPrompt,
688
700
  reviewFixPoints: config.reviewFixPoints,
701
+ mdLang: config.mdLang,
702
+ llmExecutor: launchProfile.executor,
703
+ llmModel: launchProfile.model,
704
+ launchProfile,
705
+ iteration,
706
+ latestIteration,
707
+ taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
708
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
709
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
710
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
711
+ ...(latestIteration !== null ? { reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration) } : {}),
689
712
  forceRefresh: forceRefreshSummary,
690
713
  };
691
714
  }
@@ -710,88 +733,13 @@ function flowRequiresTaskScope(entry) {
710
733
  }
711
734
  return valueReferencesTaskScopeParams(entry.flow.phases);
712
735
  }
713
- async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false) {
714
- const context = createPipelineContext({
715
- issueKey: config.taskKey,
716
- jiraRef: config.jiraRef,
717
- dryRun: config.dryRun,
718
- verbose: config.verbose,
719
- runtime: runtimeServices,
720
- ...(setSummary ? { setSummary } : {}),
721
- requestUserInput: requestUserInputInTerminal,
722
- });
723
- const autoFlow = loadAutoFlow();
724
- const phase = findPhaseById(autoFlow.phases, phaseId);
725
- publishFlowState("auto", executionState);
726
- try {
727
- const result = await runExpandedPhase(phase, context, autoFlowParams(config, forceRefreshSummary), autoFlow.constants, {
728
- executionState,
729
- flowKind: autoFlow.kind,
730
- flowVersion: autoFlow.version,
731
- onStateChange: async (state) => {
732
- publishFlowState("auto", state);
733
- },
734
- onStepStart: async (_phase, step) => {
735
- if (!state) {
736
- return;
737
- }
738
- state.currentStep = `${phaseId}:${step.id}`;
739
- saveAutoPipelineState(state);
740
- },
741
- });
742
- if (state) {
743
- state.executionState = result.executionState;
744
- syncAndSaveAutoPipelineState(state);
745
- }
746
- return result.status === "skipped" ? "skipped" : "done";
747
- }
748
- catch (error) {
749
- if (!config.dryRun) {
750
- const output = String(error.output ?? "");
751
- if (output.trim()) {
752
- printError("Build verification failed");
753
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
754
- }
755
- }
756
- throw error;
757
- }
758
- }
759
- function rewindAutoPipelineState(state, phaseId) {
760
- const targetPhaseId = validateAutoPhaseId(phaseId);
761
- let phaseSeen = false;
762
- for (const step of state.steps) {
763
- if (step.id === targetPhaseId) {
764
- phaseSeen = true;
765
- }
766
- if (phaseSeen) {
767
- step.status = "pending";
768
- step.startedAt = null;
769
- step.finishedAt = null;
770
- step.returnCode = null;
771
- step.note = null;
772
- }
773
- else {
774
- step.status = "done";
775
- step.returnCode = 0;
776
- step.finishedAt ??= nowIso8601();
777
- }
778
- }
779
- state.status = "pending";
780
- state.currentStep = null;
781
- state.lastError = null;
782
- const targetIndex = state.executionState.phases.findIndex((phase) => phase.id === targetPhaseId);
783
- if (targetIndex >= 0) {
784
- state.executionState.phases = state.executionState.phases.slice(0, targetIndex);
785
- }
786
- state.executionState.terminated = false;
787
- delete state.executionState.terminationReason;
788
- }
789
736
  async function summarizeBuildFailure(output) {
790
737
  return summarizeBuildFailureViaPipeline(createPipelineContext({
791
738
  issueKey: "build-failure-summary",
792
739
  jiraRef: "build-failure-summary",
793
740
  dryRun: false,
794
741
  verbose: false,
742
+ mdLang: null,
795
743
  runtime: runtimeServices,
796
744
  requestUserInput: requestUserInputInTerminal,
797
745
  }), output);
@@ -801,27 +749,78 @@ function requireJiraConfig(config) {
801
749
  throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
802
750
  }
803
751
  }
804
- async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart") {
752
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", launchProfile, runtime = runtimeServices) {
805
753
  const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
806
- if (config.command === "auto") {
807
- if (launchMode === "restart") {
808
- resetAutoPipelineState(config);
754
+ if (config.command === "auto-golang") {
755
+ requireJiraConfig(config);
756
+ checkAutoPrerequisites(config);
757
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
758
+ process.env.JIRA_API_URL = config.jiraApiUrl;
759
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile;
760
+ let effectiveLaunchMode = launchMode;
761
+ let effectiveLaunchProfile = launchProfile;
762
+ if (config.autoFromPhase) {
763
+ const flow = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
764
+ const persistedState = loadFlowRunState(config.scope.scopeKey, "auto-golang");
765
+ if (!persistedState) {
766
+ throw new TaskRunnerError(`Cannot restart auto-golang from phase '${config.autoFromPhase}' because persisted flow state was not found.`);
767
+ }
768
+ rewindFlowRunStateToPhase(persistedState, flow.phases, config.autoFromPhase);
769
+ saveFlowRunState(persistedState);
770
+ effectiveLaunchMode = "resume";
771
+ effectiveLaunchProfile ??= persistedState.launchProfile;
772
+ printPanel("Auto-Golang Resume", `Auto-golang pipeline will continue from phase: ${config.autoFromPhase}`, "yellow");
809
773
  }
810
- await runAutoPipeline(config, setSummary, forceRefreshSummary);
774
+ await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveLaunchProfile ? { launchProfile: effectiveLaunchProfile } : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
775
+ return false;
776
+ }
777
+ if (config.command === "auto-common") {
778
+ requireJiraConfig(config);
779
+ checkAutoPrerequisites(config);
780
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
781
+ process.env.JIRA_API_URL = config.jiraApiUrl;
782
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile;
783
+ await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
811
784
  return false;
812
785
  }
813
786
  if (config.command === "auto-status") {
814
- const state = loadAutoPipelineState(config);
787
+ const state = loadFlowRunState(config.scope.scopeKey, "auto-golang");
815
788
  if (!state) {
816
- printPanel("Auto Status", `No auto state file found for ${config.taskKey}.`, "yellow");
789
+ printPanel("Auto-Golang Status", `No flow state file found for ${config.taskKey}.`, "yellow");
817
790
  return false;
818
791
  }
819
- printAutoState(state);
792
+ const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
793
+ const phaseOrder = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases;
794
+ const lines = [
795
+ `Issue: ${config.taskKey}`,
796
+ `Status: ${state.status}`,
797
+ `Current step: ${currentStep}`,
798
+ `Updated: ${state.updatedAt}`,
799
+ ];
800
+ if (state.launchProfile) {
801
+ lines.push(`Launch profile: ${state.launchProfile.executor} / ${state.launchProfile.model}`);
802
+ }
803
+ if (state.lastError) {
804
+ lines.push(`Last error: ${state.lastError.step ?? "-"} (exit ${state.lastError.returnCode ?? "-"}, ${state.lastError.message ?? "-"})`);
805
+ }
806
+ lines.push("");
807
+ for (const phase of phaseOrder) {
808
+ const phaseState = state.executionState.phases.find((candidate) => candidate.id === phase.id);
809
+ lines.push(`[${phaseState?.status ?? "pending"}] ${phase.id}`);
810
+ for (const step of phase.steps) {
811
+ const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
812
+ lines.push(` - [${stepState?.status ?? "pending"}] ${step.id}`);
813
+ }
814
+ }
815
+ if (state.executionState.terminated) {
816
+ lines.push("", `Execution terminated: ${state.executionState.terminationReason ?? "yes"}`);
817
+ }
818
+ printPanel("Auto-Golang Status", lines.join("\n"), "cyan");
820
819
  return false;
821
820
  }
822
821
  if (config.command === "auto-reset") {
823
- const removed = resetAutoPipelineState(config);
824
- printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
822
+ const removed = resetFlowRunState(config.scope.scopeKey, "auto-golang");
823
+ printPanel("Auto-Golang Reset", removed ? `State file ${flowStateFile(config.scope.scopeKey, "auto-golang")} removed.` : "No flow state file found.", "yellow");
825
824
  return false;
826
825
  }
827
826
  checkPrerequisites(config);
@@ -845,9 +844,13 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
845
844
  await runDeclarativeFlowBySpecFile("plan.json", config, {
846
845
  jiraApiUrl: config.jiraApiUrl,
847
846
  taskKey: config.taskKey,
847
+ taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
848
+ designIteration: nextArtifactIteration(config.taskKey, "design"),
849
+ planIteration: nextArtifactIteration(config.taskKey, "plan"),
850
+ qaIteration: nextArtifactIteration(config.taskKey, "qa"),
848
851
  extraPrompt: config.extraPrompt,
849
852
  forceRefresh: forceRefreshSummary,
850
- }, requestUserInput, setSummary, launchMode);
853
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
851
854
  return false;
852
855
  }
853
856
  if (config.command === "bug-analyze") {
@@ -857,21 +860,27 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
857
860
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
858
861
  process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
859
862
  }
860
- await runDeclarativeFlowBySpecFile("bug-analyze.json", config, {
863
+ await runDeclarativeFlowBySpecFile("bugz/bug-analyze.json", config, {
861
864
  jiraApiUrl: config.jiraApiUrl,
862
865
  taskKey: config.taskKey,
866
+ taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
867
+ bugAnalyzeIteration: nextArtifactIteration(config.taskKey, "bug-analyze"),
868
+ bugFixDesignIteration: nextArtifactIteration(config.taskKey, "bug-fix-design"),
869
+ bugFixPlanIteration: nextArtifactIteration(config.taskKey, "bug-fix-plan"),
863
870
  extraPrompt: config.extraPrompt,
864
871
  forceRefresh: forceRefreshSummary,
865
- }, requestUserInput, setSummary, launchMode);
872
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, setSummary, launchMode, runtime);
866
873
  return false;
867
874
  }
868
875
  if (config.command === "gitlab-review") {
869
876
  const iteration = nextReviewIterationForTask(config.taskKey);
870
- await runDeclarativeFlowBySpecFile("gitlab-review.json", config, {
877
+ const gitlabReviewIteration = nextArtifactIteration(config.taskKey, "gitlab-review");
878
+ await runDeclarativeFlowBySpecFile("gitlab/gitlab-review.json", config, {
871
879
  taskKey: config.taskKey,
872
880
  iteration,
881
+ gitlabReviewIteration,
873
882
  extraPrompt: config.extraPrompt,
874
- }, requestUserInput, undefined, launchMode);
883
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
875
884
  if (!config.dryRun) {
876
885
  printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}`);
877
886
  }
@@ -879,11 +888,13 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
879
888
  }
880
889
  if (config.command === "gitlab-diff-review") {
881
890
  const iteration = nextReviewIterationForTask(config.taskKey);
882
- await runDeclarativeFlowBySpecFile("gitlab-diff-review.json", config, {
891
+ const gitlabDiffIteration = nextArtifactIteration(config.taskKey, "gitlab-diff");
892
+ await runDeclarativeFlowBySpecFile("gitlab/gitlab-diff-review.json", config, {
883
893
  taskKey: config.taskKey,
884
894
  iteration,
895
+ gitlabDiffIteration,
885
896
  extraPrompt: config.extraPrompt,
886
- }, requestUserInput, undefined, launchMode);
897
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
887
898
  if (!config.dryRun) {
888
899
  printSummary("GitLab Diff Review", `Artifacts:\n${gitlabDiffFile(config.taskKey)}\n${gitlabDiffJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}`);
889
900
  }
@@ -898,28 +909,30 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
898
909
  { path: bugFixDesignJsonFile(config.taskKey), schemaId: "bug-fix-design/v1" },
899
910
  { path: bugFixPlanJsonFile(config.taskKey), schemaId: "bug-fix-plan/v1" },
900
911
  ], "Bug-fix mode requires valid structured artifacts from the bug analysis phase.");
901
- await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
912
+ await runDeclarativeFlowBySpecFile("bugz/bug-fix.json", config, {
902
913
  taskKey: config.taskKey,
903
914
  extraPrompt: config.extraPrompt,
904
- }, requestUserInput, undefined, launchMode);
915
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
905
916
  return false;
906
917
  }
907
918
  if (config.command === "mr-description") {
908
919
  requireJiraConfig(config);
909
920
  requireJiraTaskFile(config.jiraTaskFile);
910
- await runDeclarativeFlowBySpecFile("mr-description.json", config, {
921
+ await runDeclarativeFlowBySpecFile("gitlab/mr-description.json", config, {
911
922
  taskKey: config.taskKey,
923
+ iteration: nextArtifactIteration(config.taskKey, "mr-description"),
912
924
  extraPrompt: config.extraPrompt,
913
- }, requestUserInput, undefined, launchMode);
925
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
914
926
  return false;
915
927
  }
916
928
  if (config.command === "task-describe") {
917
- requireJiraConfig(config);
929
+ const iteration = nextArtifactIteration(config.taskKey, "jira-description");
918
930
  await runDeclarativeFlowBySpecFile("task-describe.json", config, {
919
931
  jiraApiUrl: config.jiraApiUrl,
920
932
  taskKey: config.taskKey,
933
+ iteration,
921
934
  extraPrompt: config.extraPrompt,
922
- }, requestUserInput, undefined, launchMode);
935
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
923
936
  return false;
924
937
  }
925
938
  if (config.command === "implement") {
@@ -932,7 +945,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
932
945
  await runDeclarativeFlowBySpecFile("implement.json", config, {
933
946
  taskKey: config.taskKey,
934
947
  extraPrompt: config.extraPrompt,
935
- }, requestUserInput, undefined, launchMode);
948
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode);
936
949
  return false;
937
950
  }
938
951
  if (config.command === "review") {
@@ -943,142 +956,73 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
943
956
  { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
944
957
  { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
945
958
  ], "Review mode requires valid structured plan artifacts from the planning phase.");
946
- await runDeclarativeFlowBySpecFile("review.json", config, {
959
+ await runDeclarativeFlowBySpecFile("review/review.json", config, {
947
960
  taskKey: config.taskKey,
948
961
  iteration,
949
962
  extraPrompt: config.extraPrompt,
950
- }, requestUserInput, undefined, launchMode);
963
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
951
964
  }
952
965
  else {
953
- await runDeclarativeFlowBySpecFile("review-project.json", config, {
966
+ await runDeclarativeFlowBySpecFile("review/review-project.json", config, {
954
967
  taskKey: config.taskKey,
955
968
  iteration,
956
969
  extraPrompt: config.extraPrompt,
957
- }, requestUserInput, undefined, launchMode);
970
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
958
971
  }
959
972
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
960
973
  }
961
974
  if (config.command === "review-fix") {
962
- const latestIteration = latestReviewReplyIteration(config.taskKey);
975
+ const latestIteration = latestArtifactIteration(config.taskKey, "review");
963
976
  if (latestIteration === null) {
964
- throw new TaskRunnerError("Review-fix mode requires at least one review-reply artifact.");
977
+ throw new TaskRunnerError("Review-fix mode requires at least one review artifact.");
965
978
  }
966
979
  validateStructuredArtifacts([
967
980
  { path: reviewJsonFile(config.taskKey, latestIteration), schemaId: "review-findings/v1" },
968
- { path: reviewReplyJsonFile(config.taskKey, latestIteration), schemaId: "review-reply/v1" },
969
981
  ], "Review-fix mode requires valid structured review artifacts.");
970
- await runDeclarativeFlowBySpecFile("review-fix.json", config, {
982
+ await runDeclarativeFlowBySpecFile("review/review-fix.json", config, {
971
983
  taskKey: config.taskKey,
972
984
  latestIteration,
973
985
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
974
986
  extraPrompt: config.extraPrompt,
975
987
  reviewFixPoints: config.reviewFixPoints,
976
- }, requestUserInput, undefined, launchMode);
988
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
977
989
  return false;
978
990
  }
991
+ if (config.command === "review-loop") {
992
+ const iteration = nextReviewIterationForTask(config.taskKey);
993
+ if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
994
+ requireJiraConfig(config);
995
+ validateStructuredArtifacts([
996
+ { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
997
+ { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
998
+ ], "Review-loop mode requires valid structured plan artifacts from the planning phase.");
999
+ }
1000
+ await runDeclarativeFlowBySpecFile("review/review-loop.json", config, {
1001
+ taskKey: config.taskKey,
1002
+ iteration,
1003
+ extraPrompt: config.extraPrompt,
1004
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1005
+ return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
1006
+ }
979
1007
  if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
980
- await runDeclarativeFlowBySpecFile(config.command === "run-go-tests-loop" ? "run-go-tests-loop.json" : "run-go-linter-loop.json", config, {
1008
+ await runDeclarativeFlowBySpecFile(config.command === "run-go-tests-loop" ? "go/run-go-tests-loop.json" : "go/run-go-linter-loop.json", config, {
981
1009
  taskKey: config.taskKey,
982
- runGoTestsScript: config.runGoTestsScript,
983
- runGoLinterScript: config.runGoLinterScript,
1010
+ runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
1011
+ runGoLinterScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_linter.py"),
1012
+ runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
1013
+ runGoLinterIteration: nextArtifactIteration(config.taskKey, "run-go-linter-result", "json"),
984
1014
  extraPrompt: config.extraPrompt,
985
- }, requestUserInput, undefined, launchMode);
1015
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
986
1016
  return false;
987
1017
  }
988
- throw new TaskRunnerError(`Unsupported command: ${config.command}`);
989
- }
990
- async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false) {
991
- checkAutoPrerequisites(config);
992
- printInfo("Dry-run auto pipeline from declarative spec");
993
- const autoFlow = loadAutoFlow();
994
- const executionState = {
995
- flowKind: autoFlow.kind,
996
- flowVersion: autoFlow.version,
997
- terminated: false,
998
- phases: [],
999
- };
1000
- publishFlowState("auto", executionState);
1001
- for (const phase of autoFlow.phases) {
1002
- printInfo(`Dry-run auto phase: ${phase.id}`);
1003
- await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary);
1004
- if (executionState.terminated) {
1005
- break;
1006
- }
1007
- }
1008
- }
1009
- async function runAutoPipeline(config, setSummary, forceRefreshSummary = false) {
1010
- requireJiraConfig(config);
1011
- if (config.dryRun) {
1012
- await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary);
1013
- return;
1014
- }
1015
- checkAutoPrerequisites(config);
1016
- process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
1017
- process.env.JIRA_API_URL = config.jiraApiUrl;
1018
- process.env.JIRA_TASK_FILE = config.jiraTaskFile;
1019
- let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
1020
- if (config.autoFromPhase) {
1021
- rewindAutoPipelineState(state, config.autoFromPhase);
1022
- printPanel("Auto Resume", `Auto pipeline will continue from phase: ${config.autoFromPhase}`, "yellow");
1023
- saveAutoPipelineState(state);
1024
- }
1025
- else if (!existsSync(autoStateFile(config.taskKey))) {
1026
- saveAutoPipelineState(state);
1027
- }
1028
- printInfo("Running auto pipeline with persisted state");
1029
- while (true) {
1030
- const step = nextAutoStep(state);
1031
- if (!step) {
1032
- syncAndSaveAutoPipelineState(state);
1033
- if (state.status === "completed") {
1034
- printPanel("Auto", "Auto pipeline finished", "green");
1035
- }
1036
- else {
1037
- printInfo(`Auto pipeline finished with status: ${state.status}`);
1038
- }
1039
- return;
1040
- }
1041
- state.status = "running";
1042
- state.currentStep = step.id;
1043
- step.status = "running";
1044
- step.startedAt = nowIso8601();
1045
- step.finishedAt = null;
1046
- step.returnCode = null;
1047
- step.note = null;
1048
- state.lastError = null;
1049
- saveAutoPipelineState(state);
1050
- try {
1051
- printInfo(`Running auto step: ${step.id}`);
1052
- const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state, setSummary, forceRefreshSummary);
1053
- step.status = status;
1054
- step.finishedAt = nowIso8601();
1055
- step.returnCode = 0;
1056
- if (status === "skipped") {
1057
- step.note = "condition not met";
1058
- }
1059
- syncAndSaveAutoPipelineState(state);
1060
- }
1061
- catch (error) {
1062
- const returnCode = Number(error.returnCode ?? 1);
1063
- step.status = "failed";
1064
- step.finishedAt = nowIso8601();
1065
- step.returnCode = returnCode;
1066
- state.status = "blocked";
1067
- state.currentStep = step.id;
1068
- state.lastError = {
1069
- step: step.id,
1070
- returnCode,
1071
- message: "command failed",
1072
- };
1073
- saveAutoPipelineState(state);
1074
- throw error;
1075
- }
1076
- if (state.executionState.terminated) {
1077
- syncAndSaveAutoPipelineState(state);
1078
- printPanel("Auto", "Auto pipeline finished", "green");
1079
- return;
1080
- }
1018
+ if (config.command === "git-commit") {
1019
+ await runDeclarativeFlowBySpecFile("git-commit.json", config, {
1020
+ taskKey: config.taskKey,
1021
+ extraPrompt: config.extraPrompt,
1022
+ }, launchProfile ? { launchProfile } : {}, requestUserInput, undefined, launchMode, runtime);
1023
+ return false;
1081
1024
  }
1025
+ throw new TaskRunnerError(`Unsupported command: ${config.command}`);
1082
1026
  }
1083
1027
  function parseCliArgs(argv) {
1084
1028
  if (argv.includes("--version") || argv.includes("-v")) {
@@ -1105,6 +1049,7 @@ function parseCliArgs(argv) {
1105
1049
  let scopeName;
1106
1050
  let helpPhases = false;
1107
1051
  let jiraRef;
1052
+ let mdLang;
1108
1053
  for (let index = 1; index < argv.length; index += 1) {
1109
1054
  const token = argv[index] ?? "";
1110
1055
  if (token === "--dry") {
@@ -1134,12 +1079,39 @@ function parseCliArgs(argv) {
1134
1079
  index += 1;
1135
1080
  continue;
1136
1081
  }
1082
+ if (token === "--md-lang") {
1083
+ const langValue = argv[index + 1];
1084
+ if (langValue === "en" || langValue === "ru") {
1085
+ mdLang = langValue;
1086
+ }
1087
+ else {
1088
+ process.stderr.write("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
1089
+ process.exit(1);
1090
+ }
1091
+ index += 1;
1092
+ continue;
1093
+ }
1094
+ if (token.startsWith("--md-lang=")) {
1095
+ const langValue = token.slice("--md-lang=".length);
1096
+ if (langValue === "en" || langValue === "ru") {
1097
+ mdLang = langValue;
1098
+ }
1099
+ else {
1100
+ process.stderr.write("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
1101
+ process.exit(1);
1102
+ }
1103
+ continue;
1104
+ }
1137
1105
  jiraRef = token;
1138
1106
  }
1139
- if (command === "auto" && helpPhases) {
1107
+ if (command === "auto-golang" && helpPhases) {
1140
1108
  printAutoPhasesHelp();
1141
1109
  process.exit(0);
1142
1110
  }
1111
+ if (command === "auto-common" && helpPhases) {
1112
+ printAutoCommonPhasesHelp();
1113
+ process.exit(0);
1114
+ }
1143
1115
  return {
1144
1116
  command: command,
1145
1117
  dry,
@@ -1149,6 +1121,7 @@ function parseCliArgs(argv) {
1149
1121
  ...(scopeName !== undefined ? { scopeName } : {}),
1150
1122
  ...(prompt !== undefined ? { prompt } : {}),
1151
1123
  ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
1124
+ ...(mdLang !== undefined ? { mdLang } : {}),
1152
1125
  };
1153
1126
  }
1154
1127
  function buildConfigFromArgs(args) {
@@ -1157,6 +1130,7 @@ function buildConfigFromArgs(args) {
1157
1130
  ...(args.scopeName !== undefined ? { scopeName: args.scopeName } : {}),
1158
1131
  ...(args.prompt !== undefined ? { extraPrompt: args.prompt } : {}),
1159
1132
  ...(args.autoFromPhase !== undefined ? { autoFromPhase: args.autoFromPhase } : {}),
1133
+ ...(args.mdLang !== undefined ? { mdLang: args.mdLang } : {}),
1160
1134
  dryRun: args.dry,
1161
1135
  verbose: args.verbose,
1162
1136
  });
@@ -1165,6 +1139,8 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1165
1139
  let currentScope = resolveProjectScope(scopeName, jiraRef);
1166
1140
  const gitBranchName = detectGitBranchName();
1167
1141
  const flowCatalog = loadInteractiveFlowCatalog(process.cwd());
1142
+ let activeAbortController = null;
1143
+ let activeFlowId = null;
1168
1144
  let exiting = false;
1169
1145
  const ui = new InteractiveUi({
1170
1146
  scopeKey: currentScope.scopeKey,
@@ -1172,43 +1148,35 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1172
1148
  summaryText: "",
1173
1149
  cwd: process.cwd(),
1174
1150
  gitBranchName,
1151
+ version: packageVersion(),
1175
1152
  flows: interactiveFlowDefinitions(flowCatalog),
1176
1153
  getRunConfirmation: async (flowId) => {
1177
1154
  const flowEntry = findCatalogEntry(flowId, flowCatalog);
1178
1155
  if (!flowEntry) {
1179
1156
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1180
1157
  }
1181
- if (flowId === "auto") {
1182
- if (!currentScope.jiraRef) {
1183
- return { resumeAvailable: false, hasExistingState: false };
1184
- }
1185
- const baseConfig = buildBaseConfig("auto", {
1186
- jiraRef: currentScope.jiraRef,
1187
- scopeName: currentScope.scopeKey,
1188
- });
1189
- const state = loadAutoPipelineState(buildRuntimeConfig(baseConfig, currentScope));
1190
- if (!state) {
1191
- return { resumeAvailable: false, hasExistingState: false };
1192
- }
1193
- const status = deriveAutoPipelineStatus(state);
1194
- if (status === "completed") {
1195
- return { resumeAvailable: false, hasExistingState: true };
1196
- }
1197
- return {
1198
- resumeAvailable: true,
1199
- hasExistingState: true,
1200
- details: buildAutoResumeDetails(state),
1201
- };
1202
- }
1203
1158
  const resumeLookup = lookupInteractiveFlowResume(flowEntry, currentScope);
1204
1159
  return resumeLookup;
1205
1160
  },
1206
1161
  onRun: async (flowId, launchMode) => {
1162
+ const abortController = new AbortController();
1163
+ activeAbortController = abortController;
1164
+ activeFlowId = flowId;
1207
1165
  try {
1208
1166
  const flowEntry = findCatalogEntry(flowId, flowCatalog);
1209
1167
  if (!flowEntry) {
1210
1168
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1211
1169
  }
1170
+ const resumeState = launchMode === "resume" ? loadFlowRunState(currentScope.scopeKey, flowId) : null;
1171
+ if (resumeState) {
1172
+ currentScope = scopeWithRestoredJiraContext(currentScope, resumeState);
1173
+ }
1174
+ const launchProfile = launchMode === "resume"
1175
+ ? resumeState?.launchProfile
1176
+ : await requestInteractiveLaunchProfile((form) => ui.requestUserInput(form));
1177
+ if (!launchProfile) {
1178
+ throw new TaskRunnerError("Resume is impossible because launch profile was not saved. Use restart.");
1179
+ }
1212
1180
  const previousScopeKey = currentScope.scopeKey;
1213
1181
  const baseConfig = buildBaseConfig(flowId, {
1214
1182
  ...(currentScope.jiraRef ? { jiraRef: currentScope.jiraRef } : {}),
@@ -1226,14 +1194,20 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1226
1194
  if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
1227
1195
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1228
1196
  }
1197
+ printPanel("Effective Launch Config", `executor: ${launchProfile.executor}\nmodel: ${launchProfile.model}\nmode: ${launchMode}`, "cyan");
1229
1198
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1230
- await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode);
1199
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, launchProfile, createRuntimeServices(abortController.signal));
1231
1200
  return;
1232
1201
  }
1233
1202
  const runtimeConfig = buildRuntimeConfig(baseConfig, currentScope);
1234
- await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh), (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode);
1203
+ await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh, { launchProfile }), { launchProfile }, (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1235
1204
  }
1236
1205
  catch (error) {
1206
+ if (error instanceof FlowInterruptedError) {
1207
+ ui.appendLog(`[interrupt] ${error.message}`);
1208
+ printInfo(error.message);
1209
+ return;
1210
+ }
1237
1211
  if (error instanceof TaskRunnerError) {
1238
1212
  ui.setFlowFailed(flowId);
1239
1213
  printError(error.message);
@@ -1247,6 +1221,19 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1247
1221
  }
1248
1222
  throw error;
1249
1223
  }
1224
+ finally {
1225
+ if (activeAbortController === abortController) {
1226
+ activeAbortController = null;
1227
+ activeFlowId = null;
1228
+ }
1229
+ }
1230
+ },
1231
+ onInterrupt: async (flowId) => {
1232
+ if (!activeAbortController || activeFlowId !== flowId) {
1233
+ return;
1234
+ }
1235
+ ui.interruptActiveForm();
1236
+ activeAbortController.abort();
1250
1237
  },
1251
1238
  onExit: () => {
1252
1239
  exiting = true;
@@ -1277,7 +1264,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1277
1264
  });
1278
1265
  }
1279
1266
  export async function main(argv = process.argv.slice(2)) {
1280
- loadEnvFile(path.join(process.cwd(), ".env"));
1267
+ loadTieredEnv(process.cwd());
1281
1268
  let forceRefresh = false;
1282
1269
  const args = [...argv];
1283
1270
  if (args[0] === "--force") {