agentweaver 0.1.2 → 0.1.4

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 (61) hide show
  1. package/README.md +58 -23
  2. package/dist/artifacts.js +58 -2
  3. package/dist/executors/claude-executor.js +12 -2
  4. package/dist/executors/claude-summary-executor.js +1 -1
  5. package/dist/executors/codex-docker-executor.js +1 -1
  6. package/dist/executors/codex-local-executor.js +1 -1
  7. package/dist/executors/configs/claude-config.js +2 -1
  8. package/dist/executors/verify-build-executor.js +110 -9
  9. package/dist/index.js +466 -452
  10. package/dist/interactive-ui.js +538 -194
  11. package/dist/jira.js +3 -1
  12. package/dist/pipeline/auto-flow.js +9 -0
  13. package/dist/pipeline/checks.js +5 -0
  14. package/dist/pipeline/context.js +2 -0
  15. package/dist/pipeline/declarative-flow-runner.js +262 -0
  16. package/dist/pipeline/declarative-flows.js +24 -0
  17. package/dist/pipeline/flow-specs/auto.json +485 -0
  18. package/dist/pipeline/flow-specs/bug-analyze.json +140 -0
  19. package/dist/pipeline/flow-specs/bug-fix.json +44 -0
  20. package/dist/pipeline/flow-specs/implement.json +47 -0
  21. package/dist/pipeline/flow-specs/mr-description.json +61 -0
  22. package/dist/pipeline/flow-specs/plan.json +88 -0
  23. package/dist/pipeline/flow-specs/preflight.json +174 -0
  24. package/dist/pipeline/flow-specs/review-fix.json +76 -0
  25. package/dist/pipeline/flow-specs/review.json +233 -0
  26. package/dist/pipeline/flow-specs/run-linter-loop.json +149 -0
  27. package/dist/pipeline/flow-specs/run-tests-loop.json +149 -0
  28. package/dist/pipeline/flow-specs/task-describe.json +61 -0
  29. package/dist/pipeline/flow-specs/test-fix.json +24 -0
  30. package/dist/pipeline/flow-specs/test-linter-fix.json +24 -0
  31. package/dist/pipeline/flow-specs/test.json +19 -0
  32. package/dist/pipeline/flows/implement-flow.js +3 -4
  33. package/dist/pipeline/flows/preflight-flow.js +17 -57
  34. package/dist/pipeline/flows/review-fix-flow.js +3 -4
  35. package/dist/pipeline/flows/review-flow.js +8 -4
  36. package/dist/pipeline/flows/test-fix-flow.js +3 -4
  37. package/dist/pipeline/node-registry.js +74 -0
  38. package/dist/pipeline/node-runner.js +9 -3
  39. package/dist/pipeline/nodes/build-failure-summary-node.js +4 -4
  40. package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
  41. package/dist/pipeline/nodes/claude-summary-node.js +12 -6
  42. package/dist/pipeline/nodes/codex-docker-prompt-node.js +1 -0
  43. package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
  44. package/dist/pipeline/nodes/file-check-node.js +15 -0
  45. package/dist/pipeline/nodes/flow-run-node.js +40 -0
  46. package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
  47. package/dist/pipeline/nodes/task-summary-node.js +12 -6
  48. package/dist/pipeline/nodes/verify-build-node.js +1 -0
  49. package/dist/pipeline/prompt-registry.js +27 -0
  50. package/dist/pipeline/prompt-runtime.js +18 -0
  51. package/dist/pipeline/registry.js +0 -2
  52. package/dist/pipeline/spec-compiler.js +213 -0
  53. package/dist/pipeline/spec-loader.js +14 -0
  54. package/dist/pipeline/spec-types.js +1 -0
  55. package/dist/pipeline/spec-validator.js +302 -0
  56. package/dist/pipeline/value-resolver.js +217 -0
  57. package/dist/prompts.js +22 -3
  58. package/dist/runtime/process-runner.js +24 -23
  59. package/dist/structured-artifacts.js +178 -0
  60. package/dist/tui.js +39 -0
  61. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,46 +1,42 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { appendFile, readFile } from "node:fs/promises";
4
- import os from "node:os";
2
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
3
  import path from "node:path";
6
4
  import process from "node:process";
7
5
  import { fileURLToPath } from "node:url";
8
- import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, designFile, planArtifacts, planFile, requireArtifacts, taskSummaryFile, } from "./artifacts.js";
6
+ import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, ensureTaskWorkspaceDir, jiraTaskFile, planArtifacts, readyToMergeFile, requireArtifacts, taskWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
9
7
  import { TaskRunnerError } from "./errors.js";
10
8
  import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
11
- import { AUTO_REVIEW_FIX_EXTRA_PROMPT, IMPLEMENT_PROMPT_TEMPLATE, formatPrompt, formatTemplate, } from "./prompts.js";
9
+ import { validateStructuredArtifacts } from "./structured-artifacts.js";
12
10
  import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
13
11
  import { createPipelineContext } from "./pipeline/context.js";
14
- import { runFlow } from "./pipeline/flow-runner.js";
15
- import { implementFlowDefinition, runImplementFlow } from "./pipeline/flows/implement-flow.js";
16
- import { planFlowDefinition, runPlanFlow } from "./pipeline/flows/plan-flow.js";
12
+ import { loadAutoFlow } from "./pipeline/auto-flow.js";
13
+ import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
14
+ import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
17
15
  import { runPreflightFlow } from "./pipeline/flows/preflight-flow.js";
18
- import { createReviewFixFlowDefinition, runReviewFixFlow } from "./pipeline/flows/review-fix-flow.js";
19
- import { createReviewFlowDefinition, runReviewFlow } from "./pipeline/flows/review-flow.js";
20
- import { runTestFixFlow } from "./pipeline/flows/test-fix-flow.js";
21
- import { runTestFlow, testFlowDefinition } from "./pipeline/flows/test-flow.js";
22
16
  import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
23
17
  import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
24
18
  import { runCommand } from "./runtime/process-runner.js";
25
19
  import { InteractiveUi } from "./interactive-ui.js";
26
- import { bye, printError, printInfo, printPanel, printSummary } from "./tui.js";
20
+ import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState } from "./tui.js";
27
21
  const COMMANDS = [
22
+ "bug-analyze",
23
+ "bug-fix",
24
+ "mr-description",
28
25
  "plan",
26
+ "task-describe",
29
27
  "implement",
30
28
  "review",
31
29
  "review-fix",
32
30
  "test",
33
31
  "test-fix",
34
32
  "test-linter-fix",
33
+ "run-tests-loop",
34
+ "run-linter-loop",
35
35
  "auto",
36
36
  "auto-status",
37
37
  "auto-reset",
38
38
  ];
39
- const DEFAULT_CODEX_MODEL = "gpt-5.4";
40
- const DEFAULT_CLAUDE_REVIEW_MODEL = "opus";
41
- const HISTORY_FILE = path.join(os.homedir(), ".codex", "memories", "agentweaver-history");
42
- const AUTO_STATE_SCHEMA_VERSION = 1;
43
- const AUTO_MAX_REVIEW_ITERATIONS = 3;
39
+ const AUTO_STATE_SCHEMA_VERSION = 3;
44
40
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
45
41
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
46
42
  const runtimeServices = {
@@ -53,13 +49,19 @@ function usage() {
53
49
  return `Usage:
54
50
  agentweaver <jira-browse-url|jira-issue-key>
55
51
  agentweaver --force <jira-browse-url|jira-issue-key>
52
+ agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
53
+ agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
54
+ agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
56
55
  agentweaver plan [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
56
+ agentweaver task-describe [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
57
57
  agentweaver implement [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
58
58
  agentweaver review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
59
59
  agentweaver review-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
60
60
  agentweaver test [--dry] [--verbose] <jira-browse-url|jira-issue-key>
61
61
  agentweaver test-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
62
62
  agentweaver test-linter-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
63
+ agentweaver run-tests-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
64
+ agentweaver run-linter-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
63
65
  agentweaver auto [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
64
66
  agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> <jira-browse-url|jira-issue-key>
65
67
  agentweaver auto --help-phases
@@ -67,17 +69,18 @@ function usage() {
67
69
  agentweaver auto-reset <jira-browse-url|jira-issue-key>
68
70
 
69
71
  Interactive Mode:
70
- When started with only a Jira task, the script opens an interactive shell.
71
- Available slash commands: /plan, /implement, /review, /review-fix, /test, /test-fix, /test-linter-fix, /auto, /auto-status, /auto-reset, /help, /exit
72
+ When started with only a Jira task, the script opens an interactive UI.
73
+ Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
72
74
 
73
75
  Flags:
76
+ --version Show package version
74
77
  --force In interactive mode, force refresh Jira task and task summary
75
78
  --dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
76
79
  --verbose Show live stdout/stderr of launched commands
77
80
  --prompt Extra prompt text appended to the base prompt
78
81
 
79
82
  Required environment variables:
80
- JIRA_API_KEY Jira API key used in Authorization: Bearer <token> for plan
83
+ JIRA_API_KEY Jira API key used in Authorization: Bearer <token> for Jira-backed flows
81
84
 
82
85
  Optional environment variables:
83
86
  JIRA_BASE_URL
@@ -86,8 +89,15 @@ Optional environment variables:
86
89
  CODEX_BIN
87
90
  CODEX_MODEL
88
91
  CLAUDE_BIN
89
- CLAUDE_REVIEW_MODEL
90
- CLAUDE_SUMMARY_MODEL`;
92
+ CLAUDE_MODEL`;
93
+ }
94
+ function packageVersion() {
95
+ const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
96
+ const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
97
+ if (typeof raw.version !== "string" || !raw.version.trim()) {
98
+ throw new TaskRunnerError(`Package version is missing in ${packageJsonPath}`);
99
+ }
100
+ return raw.version;
91
101
  }
92
102
  function nowIso8601() {
93
103
  return new Date().toISOString();
@@ -95,24 +105,14 @@ function nowIso8601() {
95
105
  function normalizeAutoPhaseId(phaseId) {
96
106
  return phaseId.trim().toLowerCase().replaceAll("-", "_");
97
107
  }
98
- function buildAutoSteps(maxReviewIterations = AUTO_MAX_REVIEW_ITERATIONS) {
99
- const steps = [
100
- { id: "plan", command: "plan", status: "pending" },
101
- { id: "implement", command: "implement", status: "pending" },
102
- { id: "test_after_implement", command: "test", status: "pending" },
103
- ];
104
- for (let iteration = 1; iteration <= maxReviewIterations; iteration += 1) {
105
- steps.push({ id: `review_${iteration}`, command: "review", status: "pending", reviewIteration: iteration }, { id: `review_fix_${iteration}`, command: "review-fix", status: "pending", reviewIteration: iteration }, {
106
- id: `test_after_review_fix_${iteration}`,
107
- command: "test",
108
- status: "pending",
109
- reviewIteration: iteration,
110
- });
111
- }
112
- return steps;
108
+ function buildAutoSteps() {
109
+ return loadAutoFlow().phases.map((phase) => ({
110
+ id: phase.id,
111
+ status: "pending",
112
+ }));
113
113
  }
114
- function autoPhaseIds(maxReviewIterations = AUTO_MAX_REVIEW_ITERATIONS) {
115
- return buildAutoSteps(maxReviewIterations).map((step) => step.id);
114
+ function autoPhaseIds() {
115
+ return buildAutoSteps().map((step) => step.id);
116
116
  }
117
117
  function validateAutoPhaseId(phaseId) {
118
118
  const normalized = normalizeAutoPhaseId(phaseId);
@@ -121,19 +121,46 @@ function validateAutoPhaseId(phaseId) {
121
121
  }
122
122
  return normalized;
123
123
  }
124
- function autoStateFile(taskKey) {
125
- return path.join(process.cwd(), `.agentweaver-state-${taskKey}.json`);
126
- }
127
124
  function createAutoPipelineState(config) {
125
+ const autoFlow = loadAutoFlow();
126
+ const maxReviewIterations = autoFlow.phases.filter((phase) => /^review_\d+$/.test(phase.id)).length;
128
127
  return {
129
128
  schemaVersion: AUTO_STATE_SCHEMA_VERSION,
130
129
  issueKey: config.taskKey,
131
130
  jiraRef: config.jiraRef,
132
131
  status: "pending",
133
132
  currentStep: null,
134
- maxReviewIterations: AUTO_MAX_REVIEW_ITERATIONS,
133
+ maxReviewIterations,
135
134
  updatedAt: nowIso8601(),
136
135
  steps: buildAutoSteps(),
136
+ executionState: {
137
+ flowKind: autoFlow.kind,
138
+ flowVersion: autoFlow.version,
139
+ terminated: false,
140
+ phases: [],
141
+ },
142
+ };
143
+ }
144
+ function stripExecutionStatePayload(executionState) {
145
+ return {
146
+ flowKind: executionState.flowKind,
147
+ flowVersion: executionState.flowVersion,
148
+ terminated: executionState.terminated,
149
+ ...(executionState.terminationReason ? { terminationReason: executionState.terminationReason } : {}),
150
+ phases: executionState.phases.map((phase) => ({
151
+ id: phase.id,
152
+ status: phase.status,
153
+ repeatVars: { ...phase.repeatVars },
154
+ ...(phase.startedAt ? { startedAt: phase.startedAt } : {}),
155
+ ...(phase.finishedAt ? { finishedAt: phase.finishedAt } : {}),
156
+ steps: phase.steps.map((step) => ({
157
+ id: step.id,
158
+ status: step.status,
159
+ ...(step.startedAt ? { startedAt: step.startedAt } : {}),
160
+ ...(step.finishedAt ? { finishedAt: step.finishedAt } : {}),
161
+ ...(step.stopFlow !== undefined ? { stopFlow: step.stopFlow } : {}),
162
+ })),
163
+ })),
137
164
  };
138
165
  }
139
166
  function loadAutoPipelineState(config) {
@@ -155,11 +182,29 @@ function loadAutoPipelineState(config) {
155
182
  if (state.schemaVersion !== AUTO_STATE_SCHEMA_VERSION) {
156
183
  throw new TaskRunnerError(`Unsupported auto state schema in ${filePath}: ${state.schemaVersion}`);
157
184
  }
185
+ if (!state.executionState) {
186
+ const autoFlow = loadAutoFlow();
187
+ state.executionState = {
188
+ flowKind: autoFlow.kind,
189
+ flowVersion: autoFlow.version,
190
+ terminated: false,
191
+ phases: [],
192
+ };
193
+ }
194
+ syncAutoStepsFromExecutionState(state);
158
195
  return state;
159
196
  }
160
197
  function saveAutoPipelineState(state) {
161
198
  state.updatedAt = nowIso8601();
162
- writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify(state, null, 2)}\n`, "utf8");
199
+ ensureTaskWorkspaceDir(state.issueKey);
200
+ writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify({
201
+ ...state,
202
+ executionState: stripExecutionStatePayload(state.executionState),
203
+ }, null, 2)}\n`, "utf8");
204
+ }
205
+ function syncAndSaveAutoPipelineState(state) {
206
+ syncAutoStepsFromExecutionState(state);
207
+ saveAutoPipelineState(state);
163
208
  }
164
209
  function resetAutoPipelineState(config) {
165
210
  const filePath = autoStateFile(config.taskKey);
@@ -172,28 +217,42 @@ function resetAutoPipelineState(config) {
172
217
  function nextAutoStep(state) {
173
218
  return state.steps.find((step) => ["running", "failed", "pending"].includes(step.status)) ?? null;
174
219
  }
175
- function markAutoStepSkipped(step, note) {
176
- step.status = "skipped";
177
- step.note = note;
178
- step.finishedAt = nowIso8601();
179
- }
180
- function skipAutoStepsAfterReadyToMerge(state, currentStepId) {
181
- let seenCurrent = false;
182
- for (const step of state.steps) {
183
- if (!seenCurrent) {
184
- seenCurrent = step.id === currentStepId;
185
- continue;
186
- }
187
- if (step.status === "pending") {
188
- markAutoStepSkipped(step, "ready-to-merge detected");
220
+ function findCurrentExecutionStep(state) {
221
+ for (const phase of state.executionState.phases) {
222
+ const runningStep = phase.steps.find((step) => step.status === "running");
223
+ if (runningStep) {
224
+ return `${phase.id}:${runningStep.id}`;
189
225
  }
190
226
  }
227
+ return null;
228
+ }
229
+ function deriveAutoPipelineStatus(state) {
230
+ if (state.lastError || state.steps.some((candidate) => candidate.status === "failed")) {
231
+ return "blocked";
232
+ }
233
+ if (state.executionState.terminated) {
234
+ return "completed";
235
+ }
236
+ if (state.steps.some((candidate) => candidate.status === "running")) {
237
+ return "running";
238
+ }
239
+ if (state.steps.some((candidate) => candidate.status === "pending")) {
240
+ return "pending";
241
+ }
242
+ if (state.steps.some((candidate) => candidate.status === "skipped")) {
243
+ return "completed";
244
+ }
245
+ if (state.steps.every((candidate) => candidate.status === "done")) {
246
+ return "completed";
247
+ }
248
+ return state.status;
191
249
  }
192
250
  function printAutoState(state) {
251
+ const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
193
252
  const lines = [
194
253
  `Issue: ${state.issueKey}`,
195
- `Status: ${state.status}`,
196
- `Current step: ${state.currentStep ?? "-"}`,
254
+ `Status: ${deriveAutoPipelineStatus(state)}`,
255
+ `Current step: ${currentStep}`,
197
256
  `Updated: ${state.updatedAt}`,
198
257
  ];
199
258
  if (state.lastError) {
@@ -202,14 +261,42 @@ function printAutoState(state) {
202
261
  lines.push("");
203
262
  for (const step of state.steps) {
204
263
  lines.push(`[${step.status}] ${step.id}${step.note ? ` (${step.note})` : ""}`);
264
+ const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
265
+ for (const childStep of phaseState?.steps ?? []) {
266
+ lines.push(` - [${childStep.status}] ${childStep.id}`);
267
+ }
268
+ }
269
+ if (state.executionState.terminated) {
270
+ lines.push("", `Execution terminated: ${state.executionState.terminationReason ?? "yes"}`);
205
271
  }
206
272
  printPanel("Auto Status", lines.join("\n"), "cyan");
207
273
  }
208
- function printAutoPhasesHelp() {
209
- const phaseLines = ["Available auto phases:", "", "plan", "implement", "test_after_implement"];
210
- for (let iteration = 1; iteration <= AUTO_MAX_REVIEW_ITERATIONS; iteration += 1) {
211
- phaseLines.push(`review_${iteration}`, `review_fix_${iteration}`, `test_after_review_fix_${iteration}`);
274
+ function syncAutoStepsFromExecutionState(state) {
275
+ for (const step of state.steps) {
276
+ const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
277
+ if (!phaseState) {
278
+ continue;
279
+ }
280
+ step.status = phaseState.status;
281
+ step.startedAt = phaseState.startedAt ?? null;
282
+ step.finishedAt = phaseState.finishedAt ?? null;
283
+ step.note = null;
284
+ if (phaseState.status === "skipped") {
285
+ step.note = "condition not met";
286
+ step.returnCode ??= 0;
287
+ }
288
+ else if (phaseState.status === "done") {
289
+ step.returnCode ??= 0;
290
+ if (state.executionState.terminated && state.executionState.terminationReason?.startsWith(`Stopped by ${step.id}:`)) {
291
+ step.note = "stop condition met";
292
+ }
293
+ }
212
294
  }
295
+ state.currentStep = findCurrentExecutionStep(state);
296
+ state.status = deriveAutoPipelineStatus(state);
297
+ }
298
+ function printAutoPhasesHelp() {
299
+ const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
213
300
  phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
214
301
  printPanel("Auto Phases", phaseLines.join("\n"), "magenta");
215
302
  }
@@ -243,7 +330,11 @@ function loadEnvFile(envFilePath) {
243
330
  }
244
331
  function nextReviewIterationForTask(taskKey) {
245
332
  let maxIndex = 0;
246
- for (const entry of readdirSync(process.cwd(), { withFileTypes: true })) {
333
+ const workspaceDir = taskWorkspaceDir(taskKey);
334
+ if (!existsSync(workspaceDir)) {
335
+ return 1;
336
+ }
337
+ for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
247
338
  if (!entry.isFile()) {
248
339
  continue;
249
340
  }
@@ -256,7 +347,11 @@ function nextReviewIterationForTask(taskKey) {
256
347
  }
257
348
  function latestReviewReplyIteration(taskKey) {
258
349
  let maxIndex = null;
259
- for (const entry of readdirSync(process.cwd(), { withFileTypes: true })) {
350
+ const workspaceDir = taskWorkspaceDir(taskKey);
351
+ if (!existsSync(workspaceDir)) {
352
+ return null;
353
+ }
354
+ for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
260
355
  if (!entry.isFile()) {
261
356
  continue;
262
357
  }
@@ -270,6 +365,7 @@ function latestReviewReplyIteration(taskKey) {
270
365
  }
271
366
  function buildConfig(command, jiraRef, options = {}) {
272
367
  const jiraIssueKey = extractIssueKey(jiraRef);
368
+ ensureTaskWorkspaceDir(jiraIssueKey);
273
369
  return {
274
370
  command,
275
371
  jiraRef,
@@ -279,51 +375,101 @@ function buildConfig(command, jiraRef, options = {}) {
279
375
  dryRun: options.dryRun ?? false,
280
376
  verbose: options.verbose ?? false,
281
377
  dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
282
- codexCmd: process.env.CODEX_BIN ?? "codex",
283
- claudeCmd: process.env.CLAUDE_BIN ?? "claude",
284
378
  jiraIssueKey,
285
379
  taskKey: jiraIssueKey,
286
380
  jiraBrowseUrl: buildJiraBrowseUrl(jiraRef),
287
381
  jiraApiUrl: buildJiraApiUrl(jiraRef),
288
- jiraTaskFile: `./${jiraIssueKey}.json`,
382
+ jiraTaskFile: jiraTaskFile(jiraIssueKey),
289
383
  };
290
384
  }
291
385
  function checkPrerequisites(config) {
292
- let codexCmd = config.codexCmd;
293
- let claudeCmd = config.claudeCmd;
294
- if (config.command === "plan" || config.command === "review") {
295
- codexCmd = resolveCmd("codex", "CODEX_BIN");
386
+ if (config.command === "bug-analyze" ||
387
+ config.command === "bug-fix" ||
388
+ config.command === "mr-description" ||
389
+ config.command === "plan" ||
390
+ config.command === "task-describe" ||
391
+ config.command === "review" ||
392
+ config.command === "run-tests-loop" ||
393
+ config.command === "run-linter-loop") {
394
+ resolveCmd("codex", "CODEX_BIN");
296
395
  }
297
396
  if (config.command === "review") {
298
- claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
397
+ resolveCmd("claude", "CLAUDE_BIN");
299
398
  }
300
- if (["implement", "review-fix", "test", "test-fix", "test-linter-fix"].includes(config.command)) {
399
+ if (["implement", "review-fix", "test", "run-tests-loop", "run-linter-loop"].includes(config.command)) {
301
400
  resolveDockerComposeCmd();
302
401
  if (!existsSync(config.dockerComposeFile)) {
303
402
  throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
304
403
  }
305
404
  }
306
- return { codexCmd, claudeCmd };
307
405
  }
308
- function buildPhaseConfig(baseConfig, command) {
309
- return { ...baseConfig, command };
310
- }
311
- function appendPromptText(basePrompt, suffix) {
312
- if (!basePrompt?.trim()) {
313
- return suffix;
406
+ function checkAutoPrerequisites(config) {
407
+ resolveCmd("codex", "CODEX_BIN");
408
+ resolveCmd("claude", "CLAUDE_BIN");
409
+ resolveDockerComposeCmd();
410
+ if (!existsSync(config.dockerComposeFile)) {
411
+ throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
314
412
  }
315
- return `${basePrompt.trim()}\n${suffix}`;
316
413
  }
317
- function configForAutoStep(baseConfig, step) {
318
- if (step.command === "review-fix") {
319
- return {
320
- ...buildPhaseConfig(baseConfig, step.command),
321
- extraPrompt: appendPromptText(baseConfig.extraPrompt, AUTO_REVIEW_FIX_EXTRA_PROMPT),
322
- };
323
- }
324
- return buildPhaseConfig(baseConfig, step.command);
414
+ function autoFlowParams(config) {
415
+ return {
416
+ jiraApiUrl: config.jiraApiUrl,
417
+ taskKey: config.taskKey,
418
+ dockerComposeFile: config.dockerComposeFile,
419
+ extraPrompt: config.extraPrompt,
420
+ reviewFixPoints: config.reviewFixPoints,
421
+ };
325
422
  }
326
- async function runAutoStepViaFlow(config, step, codexCmd, claudeCmd, state) {
423
+ function declarativeFlowDefinition(id, label, fileName) {
424
+ const flow = loadDeclarativeFlow(fileName);
425
+ return {
426
+ id,
427
+ label,
428
+ phases: flow.phases.map((phase) => ({
429
+ id: phase.id,
430
+ repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
431
+ steps: phase.steps.map((step) => ({
432
+ id: step.id,
433
+ })),
434
+ })),
435
+ };
436
+ }
437
+ function autoFlowDefinition() {
438
+ const flow = loadAutoFlow();
439
+ return {
440
+ id: "auto",
441
+ label: "auto",
442
+ phases: flow.phases.map((phase) => ({
443
+ id: phase.id,
444
+ repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
445
+ steps: phase.steps.map((step) => ({
446
+ id: step.id,
447
+ })),
448
+ })),
449
+ };
450
+ }
451
+ function interactiveFlowDefinitions() {
452
+ return [
453
+ autoFlowDefinition(),
454
+ declarativeFlowDefinition("bug-analyze", "bug-analyze", "bug-analyze.json"),
455
+ declarativeFlowDefinition("bug-fix", "bug-fix", "bug-fix.json"),
456
+ declarativeFlowDefinition("mr-description", "mr-description", "mr-description.json"),
457
+ declarativeFlowDefinition("plan", "plan", "plan.json"),
458
+ declarativeFlowDefinition("task-describe", "task-describe", "task-describe.json"),
459
+ declarativeFlowDefinition("implement", "implement", "implement.json"),
460
+ declarativeFlowDefinition("review", "review", "review.json"),
461
+ declarativeFlowDefinition("review-fix", "review-fix", "review-fix.json"),
462
+ declarativeFlowDefinition("test", "test", "test.json"),
463
+ declarativeFlowDefinition("test-fix", "test-fix", "test-fix.json"),
464
+ declarativeFlowDefinition("test-linter-fix", "test-linter-fix", "test-linter-fix.json"),
465
+ declarativeFlowDefinition("run-tests-loop", "run-tests-loop", "run-tests-loop.json"),
466
+ declarativeFlowDefinition("run-linter-loop", "run-linter-loop", "run-linter-loop.json"),
467
+ ];
468
+ }
469
+ function publishFlowState(flowId, executionState) {
470
+ setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
471
+ }
472
+ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams) {
327
473
  const context = createPipelineContext({
328
474
  issueKey: config.taskKey,
329
475
  jiraRef: config.jiraRef,
@@ -331,90 +477,68 @@ async function runAutoStepViaFlow(config, step, codexCmd, claudeCmd, state) {
331
477
  verbose: config.verbose,
332
478
  runtime: runtimeServices,
333
479
  });
334
- const onStepStart = async (flowStep) => {
335
- state.currentStep = `${step.id}:${flowStep.id}`;
336
- saveAutoPipelineState(state);
480
+ const flow = loadDeclarativeFlow(fileName);
481
+ const executionState = {
482
+ flowKind: flow.kind,
483
+ flowVersion: flow.version,
484
+ terminated: false,
485
+ phases: [],
337
486
  };
338
- if (step.command === "plan") {
339
- await runFlow(planFlowDefinition, context, {
340
- jiraApiUrl: config.jiraApiUrl,
341
- jiraTaskFile: config.jiraTaskFile,
342
- taskKey: config.taskKey,
343
- codexCmd,
344
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
345
- }, { onStepStart });
346
- return false;
347
- }
348
- if (step.command === "implement") {
349
- const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
350
- design_file: designFile(config.taskKey),
351
- plan_file: planFile(config.taskKey),
352
- }), config.extraPrompt);
353
- await runFlow(implementFlowDefinition, context, {
354
- dockerComposeFile: config.dockerComposeFile,
355
- prompt: implementPrompt,
356
- runFollowupVerify: false,
357
- ...(!config.dryRun
358
- ? {
359
- onVerifyBuildFailure: async (output) => {
360
- printError("Build verification failed");
361
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
362
- },
363
- }
364
- : {}),
365
- }, { onStepStart });
366
- return false;
487
+ publishFlowState(config.command, executionState);
488
+ for (const phase of flow.phases) {
489
+ await runExpandedPhase(phase, context, flowParams, flow.constants, {
490
+ executionState,
491
+ flowKind: flow.kind,
492
+ flowVersion: flow.version,
493
+ onStateChange: async (state) => {
494
+ publishFlowState(config.command, state);
495
+ },
496
+ });
367
497
  }
368
- if (step.command === "test") {
369
- await runFlow(testFlowDefinition, context, {
370
- taskKey: config.taskKey,
371
- dockerComposeFile: config.dockerComposeFile,
372
- ...(!config.dryRun
373
- ? {
374
- onVerifyBuildFailure: async (output) => {
375
- printError("Build verification failed");
376
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
377
- },
498
+ }
499
+ async function runAutoPhaseViaSpec(config, phaseId, executionState, state) {
500
+ const context = createPipelineContext({
501
+ issueKey: config.taskKey,
502
+ jiraRef: config.jiraRef,
503
+ dryRun: config.dryRun,
504
+ verbose: config.verbose,
505
+ runtime: runtimeServices,
506
+ });
507
+ const autoFlow = loadAutoFlow();
508
+ const phase = findPhaseById(autoFlow.phases, phaseId);
509
+ publishFlowState("auto", executionState);
510
+ try {
511
+ const result = await runExpandedPhase(phase, context, autoFlowParams(config), autoFlow.constants, {
512
+ executionState,
513
+ flowKind: autoFlow.kind,
514
+ flowVersion: autoFlow.version,
515
+ onStateChange: async (state) => {
516
+ publishFlowState("auto", state);
517
+ },
518
+ onStepStart: async (_phase, step) => {
519
+ if (!state) {
520
+ return;
378
521
  }
379
- : {}),
380
- }, { onStepStart });
381
- return false;
522
+ state.currentStep = `${phaseId}:${step.id}`;
523
+ saveAutoPipelineState(state);
524
+ },
525
+ });
526
+ if (state) {
527
+ state.executionState = result.executionState;
528
+ syncAndSaveAutoPipelineState(state);
529
+ }
530
+ return result.status === "skipped" ? "skipped" : "done";
382
531
  }
383
- if (step.command === "review") {
384
- const iteration = step.reviewIteration ?? nextReviewIterationForTask(config.taskKey);
385
- const result = await runFlow(createReviewFlowDefinition(iteration), context, {
386
- jiraTaskFile: config.jiraTaskFile,
387
- taskKey: config.taskKey,
388
- claudeCmd,
389
- codexCmd,
390
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
391
- }, { onStepStart });
392
- return result.steps.find((candidate) => candidate.id === "check_ready_to_merge")?.result.metadata?.readyToMerge === true;
393
- }
394
- if (step.command === "review-fix") {
395
- const latestIteration = step.reviewIteration ?? latestReviewReplyIteration(config.taskKey);
396
- if (latestIteration === null) {
397
- throw new TaskRunnerError(`Review-fix mode requires at least one review-reply-${config.taskKey}-N.md artifact.`);
532
+ catch (error) {
533
+ if (!config.dryRun) {
534
+ const output = String(error.output ?? "");
535
+ if (output.trim()) {
536
+ printError("Build verification failed");
537
+ printSummary("Build Failure Summary", await summarizeBuildFailure(output));
538
+ }
398
539
  }
399
- await runFlow(createReviewFixFlowDefinition(latestIteration), context, {
400
- taskKey: config.taskKey,
401
- dockerComposeFile: config.dockerComposeFile,
402
- latestIteration,
403
- ...(config.reviewFixPoints !== undefined ? { reviewFixPoints: config.reviewFixPoints } : {}),
404
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
405
- runFollowupVerify: false,
406
- ...(!config.dryRun
407
- ? {
408
- onVerifyBuildFailure: async (output) => {
409
- printError("Build verification failed");
410
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
411
- },
412
- }
413
- : {}),
414
- }, { onStepStart });
415
- return false;
540
+ throw error;
416
541
  }
417
- throw new TaskRunnerError(`Unsupported auto step command: ${step.command}`);
418
542
  }
419
543
  function rewindAutoPipelineState(state, phaseId) {
420
544
  const targetPhaseId = validateAutoPhaseId(phaseId);
@@ -439,12 +563,12 @@ function rewindAutoPipelineState(state, phaseId) {
439
563
  state.status = "pending";
440
564
  state.currentStep = null;
441
565
  state.lastError = null;
442
- }
443
- function codexModel() {
444
- return process.env.CODEX_MODEL?.trim() || DEFAULT_CODEX_MODEL;
445
- }
446
- function claudeReviewModel() {
447
- return process.env.CLAUDE_REVIEW_MODEL?.trim() || DEFAULT_CLAUDE_REVIEW_MODEL;
566
+ const targetIndex = state.executionState.phases.findIndex((phase) => phase.id === targetPhaseId);
567
+ if (targetIndex >= 0) {
568
+ state.executionState.phases = state.executionState.phases.slice(0, targetIndex);
569
+ }
570
+ state.executionState.terminated = false;
571
+ delete state.executionState.terminationReason;
448
572
  }
449
573
  async function summarizeBuildFailure(output) {
450
574
  return summarizeBuildFailureViaPipeline(createPipelineContext({
@@ -474,163 +598,178 @@ async function executeCommand(config, runFollowupVerify = true) {
474
598
  printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
475
599
  return false;
476
600
  }
477
- const { codexCmd, claudeCmd } = checkPrerequisites(config);
601
+ checkPrerequisites(config);
478
602
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
479
603
  process.env.JIRA_API_URL = config.jiraApiUrl;
480
604
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
481
- const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
482
- design_file: designFile(config.taskKey),
483
- plan_file: planFile(config.taskKey),
484
- }), config.extraPrompt);
485
605
  if (config.command === "plan") {
486
606
  if (config.verbose) {
487
607
  process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
488
608
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
489
609
  process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
490
610
  }
491
- await runPlanFlow(createPipelineContext({
492
- issueKey: config.taskKey,
493
- jiraRef: config.jiraRef,
494
- dryRun: config.dryRun,
495
- verbose: config.verbose,
496
- runtime: runtimeServices,
497
- }), {
611
+ await runDeclarativeFlowBySpecFile("plan.json", config, {
498
612
  jiraApiUrl: config.jiraApiUrl,
499
- jiraTaskFile: config.jiraTaskFile,
500
613
  taskKey: config.taskKey,
501
- codexCmd,
502
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
614
+ extraPrompt: config.extraPrompt,
615
+ });
616
+ return false;
617
+ }
618
+ if (config.command === "bug-analyze") {
619
+ if (config.verbose) {
620
+ process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
621
+ process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
622
+ process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
623
+ }
624
+ await runDeclarativeFlowBySpecFile("bug-analyze.json", config, {
625
+ jiraApiUrl: config.jiraApiUrl,
626
+ taskKey: config.taskKey,
627
+ extraPrompt: config.extraPrompt,
628
+ });
629
+ return false;
630
+ }
631
+ if (config.command === "bug-fix") {
632
+ requireJiraTaskFile(config.jiraTaskFile);
633
+ requireArtifacts(bugAnalyzeArtifacts(config.taskKey), "Bug-fix mode requires bug-analyze artifacts from the bug analysis phase.");
634
+ validateStructuredArtifacts([
635
+ { path: bugAnalyzeJsonFile(config.taskKey), schemaId: "bug-analysis/v1" },
636
+ { path: bugFixDesignJsonFile(config.taskKey), schemaId: "bug-fix-design/v1" },
637
+ { path: bugFixPlanJsonFile(config.taskKey), schemaId: "bug-fix-plan/v1" },
638
+ ], "Bug-fix mode requires valid structured artifacts from the bug analysis phase.");
639
+ await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
640
+ taskKey: config.taskKey,
641
+ extraPrompt: config.extraPrompt,
642
+ });
643
+ return false;
644
+ }
645
+ if (config.command === "mr-description") {
646
+ requireJiraTaskFile(config.jiraTaskFile);
647
+ await runDeclarativeFlowBySpecFile("mr-description.json", config, {
648
+ taskKey: config.taskKey,
649
+ extraPrompt: config.extraPrompt,
650
+ });
651
+ return false;
652
+ }
653
+ if (config.command === "task-describe") {
654
+ requireJiraTaskFile(config.jiraTaskFile);
655
+ await runDeclarativeFlowBySpecFile("task-describe.json", config, {
656
+ taskKey: config.taskKey,
657
+ extraPrompt: config.extraPrompt,
503
658
  });
504
659
  return false;
505
660
  }
506
661
  if (config.command === "implement") {
507
662
  requireJiraTaskFile(config.jiraTaskFile);
508
663
  requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
509
- await runImplementFlow(createPipelineContext({
510
- issueKey: config.taskKey,
511
- jiraRef: config.jiraRef,
512
- dryRun: config.dryRun,
513
- verbose: config.verbose,
514
- runtime: runtimeServices,
515
- }), {
516
- dockerComposeFile: config.dockerComposeFile,
517
- prompt: implementPrompt,
518
- runFollowupVerify,
519
- ...(!config.dryRun
520
- ? {
521
- onVerifyBuildFailure: async (output) => {
522
- printError("Build verification failed");
523
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
524
- },
664
+ try {
665
+ await runDeclarativeFlowBySpecFile("implement.json", config, {
666
+ taskKey: config.taskKey,
667
+ dockerComposeFile: config.dockerComposeFile,
668
+ extraPrompt: config.extraPrompt,
669
+ runFollowupVerify,
670
+ });
671
+ }
672
+ catch (error) {
673
+ if (!config.dryRun) {
674
+ const output = String(error.output ?? "");
675
+ if (output.trim()) {
676
+ printError("Build verification failed");
677
+ printSummary("Build Failure Summary", await summarizeBuildFailure(output));
525
678
  }
526
- : {}),
527
- });
679
+ }
680
+ throw error;
681
+ }
528
682
  return false;
529
683
  }
530
684
  if (config.command === "review") {
531
685
  requireJiraTaskFile(config.jiraTaskFile);
532
- const result = await runReviewFlow(createPipelineContext({
533
- issueKey: config.taskKey,
534
- jiraRef: config.jiraRef,
535
- dryRun: config.dryRun,
536
- verbose: config.verbose,
537
- runtime: runtimeServices,
538
- }), {
539
- jiraTaskFile: config.jiraTaskFile,
686
+ const iteration = nextReviewIterationForTask(config.taskKey);
687
+ await runDeclarativeFlowBySpecFile("review.json", config, {
540
688
  taskKey: config.taskKey,
541
- claudeCmd,
542
- codexCmd,
543
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
689
+ iteration,
690
+ extraPrompt: config.extraPrompt,
544
691
  });
545
- return result.readyToMerge;
692
+ return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
546
693
  }
547
694
  if (config.command === "review-fix") {
548
695
  requireJiraTaskFile(config.jiraTaskFile);
549
- await runReviewFixFlow(createPipelineContext({
550
- issueKey: config.taskKey,
551
- jiraRef: config.jiraRef,
552
- dryRun: config.dryRun,
553
- verbose: config.verbose,
554
- runtime: runtimeServices,
555
- }), {
556
- taskKey: config.taskKey,
557
- dockerComposeFile: config.dockerComposeFile,
558
- latestIteration: latestReviewReplyIteration(config.taskKey),
559
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
560
- ...(config.reviewFixPoints !== undefined ? { reviewFixPoints: config.reviewFixPoints } : {}),
561
- runFollowupVerify,
562
- ...(!config.dryRun
563
- ? {
564
- onVerifyBuildFailure: async (output) => {
565
- printError("Build verification failed");
566
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
567
- },
696
+ try {
697
+ await runDeclarativeFlowBySpecFile("review-fix.json", config, {
698
+ taskKey: config.taskKey,
699
+ dockerComposeFile: config.dockerComposeFile,
700
+ latestIteration: latestReviewReplyIteration(config.taskKey),
701
+ runFollowupVerify,
702
+ extraPrompt: config.extraPrompt,
703
+ reviewFixPoints: config.reviewFixPoints,
704
+ });
705
+ }
706
+ catch (error) {
707
+ if (!config.dryRun) {
708
+ const output = String(error.output ?? "");
709
+ if (output.trim()) {
710
+ printError("Build verification failed");
711
+ printSummary("Build Failure Summary", await summarizeBuildFailure(output));
568
712
  }
569
- : {}),
570
- });
713
+ }
714
+ throw error;
715
+ }
571
716
  return false;
572
717
  }
573
718
  if (config.command === "test") {
574
719
  requireJiraTaskFile(config.jiraTaskFile);
575
- await runTestFlow(createPipelineContext({
576
- issueKey: config.taskKey,
577
- jiraRef: config.jiraRef,
578
- dryRun: config.dryRun,
579
- verbose: config.verbose,
580
- runtime: runtimeServices,
581
- }), {
582
- taskKey: config.taskKey,
583
- dockerComposeFile: config.dockerComposeFile,
584
- ...(!config.dryRun
585
- ? {
586
- onVerifyBuildFailure: async (output) => {
587
- printError("Build verification failed");
588
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
589
- },
720
+ try {
721
+ await runDeclarativeFlowBySpecFile("test.json", config, {
722
+ taskKey: config.taskKey,
723
+ dockerComposeFile: config.dockerComposeFile,
724
+ });
725
+ }
726
+ catch (error) {
727
+ if (!config.dryRun) {
728
+ const output = String(error.output ?? "");
729
+ if (output.trim()) {
730
+ printError("Build verification failed");
731
+ printSummary("Build Failure Summary", await summarizeBuildFailure(output));
590
732
  }
591
- : {}),
592
- });
733
+ }
734
+ throw error;
735
+ }
593
736
  return false;
594
737
  }
595
738
  if (config.command === "test-fix" || config.command === "test-linter-fix") {
596
739
  requireJiraTaskFile(config.jiraTaskFile);
597
- await runTestFixFlow(createPipelineContext({
598
- issueKey: config.taskKey,
599
- jiraRef: config.jiraRef,
600
- dryRun: config.dryRun,
601
- verbose: config.verbose,
602
- runtime: runtimeServices,
603
- }), {
604
- command: config.command,
740
+ await runDeclarativeFlowBySpecFile(config.command === "test-fix" ? "test-fix.json" : "test-linter-fix.json", config, {
741
+ taskKey: config.taskKey,
742
+ extraPrompt: config.extraPrompt,
743
+ });
744
+ return false;
745
+ }
746
+ if (config.command === "run-tests-loop" || config.command === "run-linter-loop") {
747
+ await runDeclarativeFlowBySpecFile(config.command === "run-tests-loop" ? "run-tests-loop.json" : "run-linter-loop.json", config, {
605
748
  taskKey: config.taskKey,
606
749
  dockerComposeFile: config.dockerComposeFile,
607
- ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
750
+ extraPrompt: config.extraPrompt,
608
751
  });
609
752
  return false;
610
753
  }
611
754
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
612
755
  }
613
756
  async function runAutoPipelineDryRun(config) {
614
- const { codexCmd, claudeCmd } = checkPrerequisites(config);
615
- printInfo("Dry-run auto pipeline: plan -> implement -> test -> review/review-fix/test");
616
- const dryState = createAutoPipelineState(config);
617
- await runAutoStepViaFlow(buildPhaseConfig(config, "plan"), dryState.steps[0], codexCmd, claudeCmd, dryState);
618
- await runAutoStepViaFlow(buildPhaseConfig(config, "implement"), dryState.steps[1], codexCmd, claudeCmd, dryState);
619
- await runAutoStepViaFlow(buildPhaseConfig(config, "test"), dryState.steps[2], codexCmd, claudeCmd, dryState);
620
- for (let iteration = 1; iteration <= AUTO_MAX_REVIEW_ITERATIONS; iteration += 1) {
621
- printInfo(`Dry-run auto review iteration ${iteration}/${AUTO_MAX_REVIEW_ITERATIONS}`);
622
- const reviewStep = dryState.steps.find((step) => step.id === `review_${iteration}`);
623
- const reviewFixStep = dryState.steps.find((step) => step.id === `review_fix_${iteration}`);
624
- const testStep = dryState.steps.find((step) => step.id === `test_after_review_fix_${iteration}`);
625
- if (!reviewStep || !reviewFixStep || !testStep) {
626
- throw new TaskRunnerError(`Missing auto dry-run steps for iteration ${iteration}`);
757
+ checkAutoPrerequisites(config);
758
+ printInfo("Dry-run auto pipeline from declarative spec");
759
+ const autoFlow = loadAutoFlow();
760
+ const executionState = {
761
+ flowKind: autoFlow.kind,
762
+ flowVersion: autoFlow.version,
763
+ terminated: false,
764
+ phases: [],
765
+ };
766
+ publishFlowState("auto", executionState);
767
+ for (const phase of autoFlow.phases) {
768
+ printInfo(`Dry-run auto phase: ${phase.id}`);
769
+ await runAutoPhaseViaSpec(config, phase.id, executionState);
770
+ if (executionState.terminated) {
771
+ break;
627
772
  }
628
- await runAutoStepViaFlow(buildPhaseConfig(config, "review"), reviewStep, codexCmd, claudeCmd, dryState);
629
- await runAutoStepViaFlow({
630
- ...buildPhaseConfig(config, "review-fix"),
631
- extraPrompt: appendPromptText(config.extraPrompt, AUTO_REVIEW_FIX_EXTRA_PROMPT),
632
- }, reviewFixStep, codexCmd, claudeCmd, dryState);
633
- await runAutoStepViaFlow(buildPhaseConfig(config, "test"), testStep, codexCmd, claudeCmd, dryState);
634
773
  }
635
774
  }
636
775
  async function runAutoPipeline(config) {
@@ -638,7 +777,10 @@ async function runAutoPipeline(config) {
638
777
  await runAutoPipelineDryRun(config);
639
778
  return;
640
779
  }
641
- const { codexCmd, claudeCmd } = checkPrerequisites(config);
780
+ checkAutoPrerequisites(config);
781
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
782
+ process.env.JIRA_API_URL = config.jiraApiUrl;
783
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile;
642
784
  let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
643
785
  if (config.autoFromPhase) {
644
786
  rewindAutoPipelineState(state, config.autoFromPhase);
@@ -652,17 +794,7 @@ async function runAutoPipeline(config) {
652
794
  while (true) {
653
795
  const step = nextAutoStep(state);
654
796
  if (!step) {
655
- if (state.steps.some((candidate) => candidate.status === "failed")) {
656
- state.status = "blocked";
657
- }
658
- else if (state.steps.some((candidate) => candidate.status === "skipped")) {
659
- state.status = "completed";
660
- }
661
- else {
662
- state.status = "max-iterations-reached";
663
- }
664
- state.currentStep = null;
665
- saveAutoPipelineState(state);
797
+ syncAndSaveAutoPipelineState(state);
666
798
  if (state.status === "completed") {
667
799
  printPanel("Auto", "Auto pipeline finished", "green");
668
800
  }
@@ -682,18 +814,14 @@ async function runAutoPipeline(config) {
682
814
  saveAutoPipelineState(state);
683
815
  try {
684
816
  printInfo(`Running auto step: ${step.id}`);
685
- const readyToMerge = await runAutoStepViaFlow(configForAutoStep(config, step), step, codexCmd, claudeCmd, state);
686
- step.status = "done";
817
+ const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state);
818
+ step.status = status;
687
819
  step.finishedAt = nowIso8601();
688
820
  step.returnCode = 0;
689
- if (step.command === "review" && readyToMerge) {
690
- skipAutoStepsAfterReadyToMerge(state, step.id);
691
- state.status = "completed";
692
- state.currentStep = null;
693
- saveAutoPipelineState(state);
694
- printPanel("Auto", "Auto pipeline finished", "green");
695
- return;
821
+ if (status === "skipped") {
822
+ step.note = "condition not met";
696
823
  }
824
+ syncAndSaveAutoPipelineState(state);
697
825
  }
698
826
  catch (error) {
699
827
  const returnCode = Number(error.returnCode ?? 1);
@@ -710,46 +838,18 @@ async function runAutoPipeline(config) {
710
838
  saveAutoPipelineState(state);
711
839
  throw error;
712
840
  }
713
- saveAutoPipelineState(state);
714
- }
715
- }
716
- function splitArgs(input) {
717
- const result = [];
718
- let current = "";
719
- let quote = null;
720
- for (let index = 0; index < input.length; index += 1) {
721
- const char = input[index] ?? "";
722
- if (quote) {
723
- if (char === quote) {
724
- quote = null;
725
- }
726
- else {
727
- current += char;
728
- }
729
- continue;
730
- }
731
- if (char === "'" || char === '"') {
732
- quote = char;
733
- continue;
734
- }
735
- if (/\s/.test(char)) {
736
- if (current) {
737
- result.push(current);
738
- current = "";
739
- }
740
- continue;
841
+ if (state.executionState.terminated) {
842
+ syncAndSaveAutoPipelineState(state);
843
+ printPanel("Auto", "Auto pipeline finished", "green");
844
+ return;
741
845
  }
742
- current += char;
743
- }
744
- if (quote) {
745
- throw new TaskRunnerError("Cannot parse command: unterminated quote");
746
- }
747
- if (current) {
748
- result.push(current);
749
846
  }
750
- return result;
751
847
  }
752
848
  function parseCliArgs(argv) {
849
+ if (argv.includes("--version") || argv.includes("-v")) {
850
+ process.stdout.write(`${packageVersion()}\n`);
851
+ process.exit(0);
852
+ }
753
853
  if (argv.includes("--help") || argv.includes("-h")) {
754
854
  process.stdout.write(`${usage()}\n`);
755
855
  process.exit(0);
@@ -821,143 +921,45 @@ function buildConfigFromArgs(args) {
821
921
  verbose: args.verbose,
822
922
  });
823
923
  }
824
- function interactiveHelp() {
825
- printPanel("Interactive Commands", [
826
- "/plan [extra prompt]",
827
- "/implement [extra prompt]",
828
- "/review [extra prompt]",
829
- "/review-fix [extra prompt]",
830
- "/test",
831
- "/test-fix [extra prompt]",
832
- "/test-linter-fix [extra prompt]",
833
- "/auto [extra prompt]",
834
- "/auto --from <phase> [extra prompt]",
835
- "/auto-status",
836
- "/auto-reset",
837
- "/help auto",
838
- "/help",
839
- "/exit",
840
- ].join("\n"), "magenta");
841
- }
842
- function parseInteractiveCommand(line, jiraRef) {
843
- const parts = splitArgs(line);
844
- if (parts.length === 0) {
845
- return null;
846
- }
847
- const command = parts[0] ?? "";
848
- if (!command.startsWith("/")) {
849
- throw new TaskRunnerError("Interactive mode expects slash commands. Use /help.");
850
- }
851
- const commandName = command.slice(1);
852
- if (commandName === "help") {
853
- if (parts[1] === "auto" || parts[1] === "phases") {
854
- printAutoPhasesHelp();
855
- return null;
856
- }
857
- interactiveHelp();
858
- return null;
859
- }
860
- if (commandName === "exit" || commandName === "quit") {
861
- throw new EOFError();
862
- }
863
- if (!COMMANDS.includes(commandName)) {
864
- throw new TaskRunnerError(`Unknown command: ${command}`);
865
- }
866
- if (commandName === "auto") {
867
- let autoFromPhase;
868
- let extraParts = parts.slice(1);
869
- if (extraParts[0] === "--from") {
870
- if (!extraParts[1]) {
871
- throw new TaskRunnerError("auto --from requires a phase name. Use /help auto.");
872
- }
873
- autoFromPhase = extraParts[1];
874
- extraParts = extraParts.slice(2);
875
- }
876
- return buildConfig("auto", jiraRef, {
877
- extraPrompt: extraParts.join(" ") || null,
878
- ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
879
- });
880
- }
881
- return buildConfig(commandName, jiraRef, {
882
- extraPrompt: parts.slice(1).join(" ") || null,
883
- });
884
- }
885
- class EOFError extends Error {
886
- }
887
- async function ensureHistoryFile() {
888
- mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
889
- if (!existsSync(HISTORY_FILE)) {
890
- writeFileSync(HISTORY_FILE, "", "utf8");
891
- }
892
- }
893
924
  async function runInteractive(jiraRef, forceRefresh = false) {
894
925
  const config = buildConfig("plan", jiraRef);
895
926
  const jiraTaskPath = config.jiraTaskFile;
896
- await ensureHistoryFile();
897
- const historyLines = (await readFile(HISTORY_FILE, "utf8"))
898
- .split(/\r?\n/)
899
- .filter(Boolean)
900
- .slice(-200);
901
927
  let exiting = false;
902
- const commandList = [
903
- "/plan",
904
- "/implement",
905
- "/review",
906
- "/review-fix",
907
- "/test",
908
- "/test-fix",
909
- "/test-linter-fix",
910
- "/auto",
911
- "/auto-status",
912
- "/auto-reset",
913
- "/help",
914
- "/exit",
915
- ];
916
928
  const ui = new InteractiveUi({
917
929
  issueKey: config.jiraIssueKey,
918
930
  summaryText: "Starting interactive session...",
919
931
  cwd: process.cwd(),
920
- commands: commandList,
921
- onSubmit: async (line) => {
932
+ flows: interactiveFlowDefinitions(),
933
+ onRun: async (flowId) => {
922
934
  try {
923
- await appendFile(HISTORY_FILE, `${line.trim()}\n`, "utf8");
924
- const command = parseInteractiveCommand(line, jiraRef);
925
- if (!command) {
926
- return;
927
- }
928
- ui.setBusy(true, command.command);
935
+ const command = buildConfig(flowId, jiraRef);
929
936
  await executeCommand(command);
930
937
  }
931
938
  catch (error) {
932
- if (error instanceof EOFError) {
933
- exiting = true;
934
- return;
935
- }
936
939
  if (error instanceof TaskRunnerError) {
940
+ ui.setFlowFailed(flowId);
937
941
  printError(error.message);
938
942
  return;
939
943
  }
940
944
  const returnCode = Number(error.returnCode);
941
945
  if (!Number.isNaN(returnCode)) {
946
+ ui.setFlowFailed(flowId);
942
947
  printError(`Command failed with exit code ${returnCode}`);
943
948
  return;
944
949
  }
945
950
  throw error;
946
951
  }
947
- finally {
948
- ui.setBusy(false);
949
- }
950
952
  },
951
953
  onExit: () => {
952
954
  exiting = true;
953
955
  },
954
- }, historyLines);
956
+ });
955
957
  ui.mount();
956
958
  printInfo(`Interactive mode for ${config.jiraIssueKey}`);
957
- printInfo("Use /help to see commands.");
959
+ printInfo("Use h to see help.");
958
960
  try {
959
961
  ui.setBusy(true, "preflight");
960
- await runPreflightFlow(createPipelineContext({
962
+ const preflightState = await runPreflightFlow(createPipelineContext({
961
963
  issueKey: config.taskKey,
962
964
  jiraRef: config.jiraRef,
963
965
  dryRun: false,
@@ -972,8 +974,20 @@ async function runInteractive(jiraRef, forceRefresh = false) {
972
974
  taskKey: config.taskKey,
973
975
  forceRefresh,
974
976
  });
977
+ const preflightPhase = preflightState.phases.find((phase) => phase.id === "preflight");
978
+ if (preflightPhase) {
979
+ ui.appendLog("[preflight] completed");
980
+ for (const step of preflightPhase.steps) {
981
+ ui.appendLog(`[preflight] ${step.id}: ${step.status}`);
982
+ }
983
+ }
984
+ if (!existsSync(jiraTaskPath)) {
985
+ throw new TaskRunnerError(`Preflight finished without Jira task file: ${jiraTaskPath}\n` +
986
+ "Jira fetch did not complete successfully. Check JIRA_API_KEY and Jira connectivity.");
987
+ }
975
988
  if (!existsSync(taskSummaryFile(config.taskKey))) {
976
- ui.setSummary("Task summary is not available yet. Run `/plan` or refresh Jira data.");
989
+ ui.appendLog("[preflight] task summary file was not created");
990
+ ui.setSummary("Task summary is not available yet. Select and run `plan` or refresh Jira data.");
977
991
  }
978
992
  }
979
993
  catch (error) {