agentweaver 0.1.2 → 0.1.3

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