agentweaver 0.1.0 → 0.1.2

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 +24 -0
  2. package/dist/executors/claude-executor.js +36 -0
  3. package/dist/executors/claude-summary-executor.js +31 -0
  4. package/dist/executors/codex-docker-executor.js +27 -0
  5. package/dist/executors/codex-local-executor.js +25 -0
  6. package/dist/executors/command-check-executor.js +14 -0
  7. package/dist/executors/configs/claude-config.js +11 -0
  8. package/dist/executors/configs/claude-summary-config.js +8 -0
  9. package/dist/executors/configs/codex-docker-config.js +10 -0
  10. package/dist/executors/configs/codex-local-config.js +8 -0
  11. package/dist/executors/configs/jira-fetch-config.js +4 -0
  12. package/dist/executors/configs/process-config.js +3 -0
  13. package/dist/executors/configs/verify-build-config.js +7 -0
  14. package/dist/executors/jira-fetch-executor.js +11 -0
  15. package/dist/executors/process-executor.js +21 -0
  16. package/dist/executors/types.js +1 -0
  17. package/dist/executors/verify-build-executor.js +22 -0
  18. package/dist/index.js +270 -450
  19. package/dist/interactive-ui.js +109 -12
  20. package/dist/pipeline/build-failure-summary.js +6 -0
  21. package/dist/pipeline/checks.js +15 -0
  22. package/dist/pipeline/context.js +17 -0
  23. package/dist/pipeline/flow-runner.js +13 -0
  24. package/dist/pipeline/flow-types.js +1 -0
  25. package/dist/pipeline/flows/implement-flow.js +48 -0
  26. package/dist/pipeline/flows/plan-flow.js +42 -0
  27. package/dist/pipeline/flows/preflight-flow.js +59 -0
  28. package/dist/pipeline/flows/review-fix-flow.js +63 -0
  29. package/dist/pipeline/flows/review-flow.js +120 -0
  30. package/dist/pipeline/flows/test-fix-flow.js +13 -0
  31. package/dist/pipeline/flows/test-flow.js +32 -0
  32. package/dist/pipeline/node-runner.js +14 -0
  33. package/dist/pipeline/nodes/build-failure-summary-node.js +71 -0
  34. package/dist/pipeline/nodes/claude-summary-node.js +32 -0
  35. package/dist/pipeline/nodes/codex-docker-prompt-node.js +31 -0
  36. package/dist/pipeline/nodes/command-check-node.js +10 -0
  37. package/dist/pipeline/nodes/implement-codex-node.js +16 -0
  38. package/dist/pipeline/nodes/jira-fetch-node.js +25 -0
  39. package/dist/pipeline/nodes/plan-codex-node.js +32 -0
  40. package/dist/pipeline/nodes/review-claude-node.js +38 -0
  41. package/dist/pipeline/nodes/review-reply-codex-node.js +40 -0
  42. package/dist/pipeline/nodes/task-summary-node.js +36 -0
  43. package/dist/pipeline/nodes/verify-build-node.js +14 -0
  44. package/dist/pipeline/registry.js +25 -0
  45. package/dist/pipeline/types.js +10 -0
  46. package/dist/runtime/command-resolution.js +139 -0
  47. package/dist/runtime/docker-runtime.js +51 -0
  48. package/dist/runtime/process-runner.js +111 -0
  49. package/dist/tui.js +34 -0
  50. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1,17 +1,29 @@
1
1
  #!/usr/bin/env node
2
- import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { appendFile, readFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
- import { spawn, spawnSync } from "node:child_process";
8
7
  import { fileURLToPath } from "node:url";
9
- import { READY_TO_MERGE_FILE, REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, artifactFile, designFile, planArtifacts, planFile, qaFile, requireArtifacts, taskSummaryFile, } from "./artifacts.js";
8
+ import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, designFile, planArtifacts, planFile, requireArtifacts, taskSummaryFile, } from "./artifacts.js";
10
9
  import { TaskRunnerError } from "./errors.js";
11
- import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, fetchJiraIssue, requireJiraTaskFile } from "./jira.js";
12
- import { AUTO_REVIEW_FIX_EXTRA_PROMPT, IMPLEMENT_PROMPT_TEMPLATE, PLAN_PROMPT_TEMPLATE, REVIEW_FIX_PROMPT_TEMPLATE, REVIEW_PROMPT_TEMPLATE, REVIEW_REPLY_PROMPT_TEMPLATE, REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, REVIEW_SUMMARY_PROMPT_TEMPLATE, TASK_SUMMARY_PROMPT_TEMPLATE, TEST_FIX_PROMPT_TEMPLATE, TEST_LINTER_FIX_PROMPT_TEMPLATE, formatPrompt, formatTemplate, } from "./prompts.js";
10
+ import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
11
+ import { AUTO_REVIEW_FIX_EXTRA_PROMPT, IMPLEMENT_PROMPT_TEMPLATE, formatPrompt, formatTemplate, } from "./prompts.js";
12
+ import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
13
+ 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";
17
+ 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
+ import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
23
+ import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
24
+ import { runCommand } from "./runtime/process-runner.js";
13
25
  import { InteractiveUi } from "./interactive-ui.js";
14
- import { bye, dim, formatDone, getOutputAdapter, printError, printInfo, printPanel, printPrompt, printSummary } from "./tui.js";
26
+ import { bye, printError, printInfo, printPanel, printSummary } from "./tui.js";
15
27
  const COMMANDS = [
16
28
  "plan",
17
29
  "implement",
@@ -26,12 +38,17 @@ const COMMANDS = [
26
38
  ];
27
39
  const DEFAULT_CODEX_MODEL = "gpt-5.4";
28
40
  const DEFAULT_CLAUDE_REVIEW_MODEL = "opus";
29
- const DEFAULT_CLAUDE_SUMMARY_MODEL = "haiku";
30
41
  const HISTORY_FILE = path.join(os.homedir(), ".codex", "memories", "agentweaver-history");
31
42
  const AUTO_STATE_SCHEMA_VERSION = 1;
32
43
  const AUTO_MAX_REVIEW_ITERATIONS = 3;
33
44
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
34
45
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
46
+ const runtimeServices = {
47
+ resolveCmd,
48
+ resolveDockerComposeCmd,
49
+ dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
50
+ runCommand,
51
+ };
35
52
  function usage() {
36
53
  return `Usage:
37
54
  agentweaver <jira-browse-url|jira-issue-key>
@@ -224,110 +241,6 @@ function loadEnvFile(envFilePath) {
224
241
  process.env[key] = value;
225
242
  }
226
243
  }
227
- function agentweaverHome() {
228
- const configured = process.env.AGENTWEAVER_HOME?.trim();
229
- if (configured) {
230
- return path.resolve(configured);
231
- }
232
- return PACKAGE_ROOT;
233
- }
234
- function defaultDockerComposeFile() {
235
- return path.join(agentweaverHome(), "docker-compose.yml");
236
- }
237
- function defaultCodexHomeDir() {
238
- return path.join(agentweaverHome(), ".codex-home");
239
- }
240
- function ensureRuntimeBindPath(targetPath, isDir) {
241
- mkdirSync(path.dirname(targetPath), { recursive: true });
242
- if (isDir) {
243
- mkdirSync(targetPath, { recursive: true });
244
- }
245
- else if (!existsSync(targetPath)) {
246
- writeFileSync(targetPath, "", "utf8");
247
- }
248
- return targetPath;
249
- }
250
- function defaultHostSshDir() {
251
- const candidate = path.join(os.homedir(), ".ssh");
252
- if (existsSync(candidate)) {
253
- return candidate;
254
- }
255
- return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "ssh"), true);
256
- }
257
- function defaultHostGitconfig() {
258
- const candidate = path.join(os.homedir(), ".gitconfig");
259
- if (existsSync(candidate)) {
260
- return candidate;
261
- }
262
- return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "gitconfig"), false);
263
- }
264
- function dockerRuntimeEnv() {
265
- const env = { ...process.env };
266
- env.AGENTWEAVER_HOME ??= agentweaverHome();
267
- env.PROJECT_DIR ??= process.cwd();
268
- env.CODEX_HOME_DIR ??= ensureRuntimeBindPath(defaultCodexHomeDir(), true);
269
- env.HOST_SSH_DIR ??= defaultHostSshDir();
270
- env.HOST_GITCONFIG ??= defaultHostGitconfig();
271
- env.LOCAL_UID ??= typeof process.getuid === "function" ? String(process.getuid()) : "1000";
272
- env.LOCAL_GID ??= typeof process.getgid === "function" ? String(process.getgid()) : "1000";
273
- return env;
274
- }
275
- function commandExists(commandName) {
276
- const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { stdio: "ignore" });
277
- return result.status === 0;
278
- }
279
- function shellQuote(value) {
280
- return `'${value.replaceAll("'", `'\\''`)}'`;
281
- }
282
- function resolveCmd(commandName, envVarName) {
283
- const configuredPath = process.env[envVarName];
284
- if (configuredPath) {
285
- accessSync(configuredPath, constants.X_OK);
286
- return configuredPath;
287
- }
288
- const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { encoding: "utf8" });
289
- if (result.status === 0 && result.stdout.trim()) {
290
- return result.stdout.trim().split(/\r?\n/)[0] ?? commandName;
291
- }
292
- throw new TaskRunnerError(`Missing required command: ${commandName}`);
293
- }
294
- function requireDockerCompose() {
295
- if (!commandExists("docker")) {
296
- throw new TaskRunnerError("Missing required command: docker");
297
- }
298
- const result = spawnSync("docker", ["compose", "version"], { stdio: "ignore" });
299
- if (result.status !== 0) {
300
- throw new TaskRunnerError("Missing required docker compose plugin");
301
- }
302
- }
303
- function resolveDockerComposeCmd() {
304
- const configured = process.env.DOCKER_COMPOSE_BIN?.trim() ?? "";
305
- if (configured) {
306
- const parts = splitArgs(configured);
307
- if (parts.length === 0) {
308
- throw new TaskRunnerError("DOCKER_COMPOSE_BIN is set but empty.");
309
- }
310
- const executable = parts[0] ?? "";
311
- try {
312
- if (path.isAbsolute(executable)) {
313
- accessSync(executable, constants.X_OK);
314
- return parts;
315
- }
316
- }
317
- catch {
318
- throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
319
- }
320
- if (commandExists(executable)) {
321
- return parts;
322
- }
323
- throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
324
- }
325
- if (commandExists("docker-compose")) {
326
- return ["docker-compose"];
327
- }
328
- requireDockerCompose();
329
- return ["docker", "compose"];
330
- }
331
244
  function nextReviewIterationForTask(taskKey) {
332
245
  let maxIndex = 0;
333
246
  for (const entry of readdirSync(process.cwd(), { withFileTypes: true })) {
@@ -355,110 +268,6 @@ function latestReviewReplyIteration(taskKey) {
355
268
  }
356
269
  return maxIndex;
357
270
  }
358
- function formatCommand(argv, env) {
359
- const envParts = Object.entries(env ?? {})
360
- .filter(([key, value]) => value !== undefined && process.env[key] !== value)
361
- .map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
362
- const command = argv.map(shellQuote).join(" ");
363
- return envParts.length > 0 ? `${envParts.join(" ")} ${command}` : command;
364
- }
365
- function formatDuration(ms) {
366
- const totalSeconds = Math.max(0, Math.floor(ms / 1000));
367
- const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
368
- const seconds = String(totalSeconds % 60).padStart(2, "0");
369
- return `${minutes}:${seconds}`;
370
- }
371
- async function runCommand(argv, options = {}) {
372
- const { env, dryRun = false, verbose = false, label, printFailureOutput = true } = options;
373
- const outputAdapter = getOutputAdapter();
374
- if (dryRun) {
375
- outputAdapter.writeStdout(`${formatCommand(argv, env)}\n`);
376
- return "";
377
- }
378
- if (verbose && outputAdapter.supportsPassthrough) {
379
- await new Promise((resolve, reject) => {
380
- const child = spawn(argv[0] ?? "", argv.slice(1), {
381
- stdio: "inherit",
382
- env,
383
- });
384
- child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(String(code ?? 1)))));
385
- child.on("error", reject);
386
- }).catch((error) => {
387
- const code = Number.parseInt(error.message, 10);
388
- throw Object.assign(new Error(`Command failed with exit code ${Number.isNaN(code) ? 1 : code}`), {
389
- returnCode: Number.isNaN(code) ? 1 : code,
390
- output: "",
391
- });
392
- });
393
- return "";
394
- }
395
- const startedAt = Date.now();
396
- const statusLabel = label ?? path.basename(argv[0] ?? argv.join(" "));
397
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
398
- let frameIndex = 0;
399
- let output = "";
400
- const child = spawn(argv[0] ?? "", argv.slice(1), {
401
- env,
402
- stdio: ["ignore", "pipe", "pipe"],
403
- });
404
- child.stdout?.on("data", (chunk) => {
405
- const text = String(chunk);
406
- output += text;
407
- if (!outputAdapter.supportsTransientStatus || verbose) {
408
- outputAdapter.writeStdout(text);
409
- }
410
- });
411
- child.stderr?.on("data", (chunk) => {
412
- const text = String(chunk);
413
- output += text;
414
- if (!outputAdapter.supportsTransientStatus || verbose) {
415
- outputAdapter.writeStderr(text);
416
- }
417
- });
418
- if (!outputAdapter.supportsTransientStatus) {
419
- outputAdapter.writeStdout(`Running ${statusLabel}\n`);
420
- }
421
- const timer = outputAdapter.supportsTransientStatus
422
- ? setInterval(() => {
423
- const elapsed = formatDuration(Date.now() - startedAt);
424
- process.stdout.write(`\r${frames[frameIndex]} ${statusLabel} ${dim(elapsed)}`);
425
- frameIndex = (frameIndex + 1) % frames.length;
426
- }, 200)
427
- : null;
428
- try {
429
- const exitCode = await new Promise((resolve, reject) => {
430
- child.on("error", reject);
431
- child.on("exit", (code) => resolve(code ?? 1));
432
- });
433
- if (timer) {
434
- clearInterval(timer);
435
- process.stdout.write(`\r${" ".repeat(80)}\r${formatDone(formatDuration(Date.now() - startedAt))}\n`);
436
- }
437
- else {
438
- outputAdapter.writeStdout(`Done ${formatDuration(Date.now() - startedAt)}\n`);
439
- }
440
- if (exitCode !== 0) {
441
- if (output && printFailureOutput) {
442
- if (outputAdapter.supportsTransientStatus) {
443
- process.stderr.write(output);
444
- if (!output.endsWith("\n")) {
445
- process.stderr.write("\n");
446
- }
447
- }
448
- }
449
- throw Object.assign(new Error(`Command failed with exit code ${exitCode}`), {
450
- returnCode: exitCode,
451
- output,
452
- });
453
- }
454
- return output;
455
- }
456
- finally {
457
- if (timer) {
458
- clearInterval(timer);
459
- }
460
- }
461
- }
462
271
  function buildConfig(command, jiraRef, options = {}) {
463
272
  const jiraIssueKey = extractIssueKey(jiraRef);
464
273
  return {
@@ -469,8 +278,7 @@ function buildConfig(command, jiraRef, options = {}) {
469
278
  autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
470
279
  dryRun: options.dryRun ?? false,
471
280
  verbose: options.verbose ?? false,
472
- dockerComposeFile: defaultDockerComposeFile(),
473
- dockerComposeCmd: [],
281
+ dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
474
282
  codexCmd: process.env.CODEX_BIN ?? "codex",
475
283
  claudeCmd: process.env.CLAUDE_BIN ?? "claude",
476
284
  jiraIssueKey,
@@ -483,7 +291,6 @@ function buildConfig(command, jiraRef, options = {}) {
483
291
  function checkPrerequisites(config) {
484
292
  let codexCmd = config.codexCmd;
485
293
  let claudeCmd = config.claudeCmd;
486
- let dockerComposeCmd = config.dockerComposeCmd;
487
294
  if (config.command === "plan" || config.command === "review") {
488
295
  codexCmd = resolveCmd("codex", "CODEX_BIN");
489
296
  }
@@ -491,12 +298,12 @@ function checkPrerequisites(config) {
491
298
  claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
492
299
  }
493
300
  if (["implement", "review-fix", "test", "test-fix", "test-linter-fix"].includes(config.command)) {
494
- dockerComposeCmd = resolveDockerComposeCmd();
301
+ resolveDockerComposeCmd();
495
302
  if (!existsSync(config.dockerComposeFile)) {
496
303
  throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
497
304
  }
498
305
  }
499
- return { codexCmd, claudeCmd, dockerComposeCmd };
306
+ return { codexCmd, claudeCmd };
500
307
  }
501
308
  function buildPhaseConfig(baseConfig, command) {
502
309
  return { ...baseConfig, command };
@@ -516,6 +323,99 @@ function configForAutoStep(baseConfig, step) {
516
323
  }
517
324
  return buildPhaseConfig(baseConfig, step.command);
518
325
  }
326
+ async function runAutoStepViaFlow(config, step, codexCmd, claudeCmd, state) {
327
+ const context = createPipelineContext({
328
+ issueKey: config.taskKey,
329
+ jiraRef: config.jiraRef,
330
+ dryRun: config.dryRun,
331
+ verbose: config.verbose,
332
+ runtime: runtimeServices,
333
+ });
334
+ const onStepStart = async (flowStep) => {
335
+ state.currentStep = `${step.id}:${flowStep.id}`;
336
+ saveAutoPipelineState(state);
337
+ };
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;
367
+ }
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
+ },
378
+ }
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;
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.`);
398
+ }
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;
416
+ }
417
+ throw new TaskRunnerError(`Unsupported auto step command: ${step.command}`);
418
+ }
519
419
  function rewindAutoPipelineState(state, phaseId) {
520
420
  const targetPhaseId = validateAutoPhaseId(phaseId);
521
421
  let phaseSeen = false;
@@ -540,120 +440,20 @@ function rewindAutoPipelineState(state, phaseId) {
540
440
  state.currentStep = null;
541
441
  state.lastError = null;
542
442
  }
543
- async function runCodexInDocker(config, dockerComposeCmd, prompt, labelText) {
544
- const dockerEnv = dockerRuntimeEnv();
545
- dockerEnv.CODEX_PROMPT = prompt;
546
- dockerEnv.CODEX_EXEC_FLAGS = `--model ${codexModel()} --dangerously-bypass-approvals-and-sandbox`;
547
- printInfo(labelText);
548
- printPrompt("Codex", prompt);
549
- await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "codex-exec"], {
550
- env: dockerEnv,
551
- dryRun: config.dryRun,
552
- verbose: config.verbose,
553
- label: `codex:${codexModel()}`,
554
- });
555
- }
556
- async function runVerifyBuildInDocker(config, dockerComposeCmd, labelText) {
557
- printInfo(labelText);
558
- try {
559
- await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "verify-build"], {
560
- env: dockerRuntimeEnv(),
561
- dryRun: config.dryRun,
562
- verbose: false,
563
- label: "verify-build",
564
- printFailureOutput: false,
565
- });
566
- }
567
- catch (error) {
568
- const returnCode = Number(error.returnCode ?? 1);
569
- printError(`Build verification failed with exit code ${returnCode}`);
570
- if (!config.dryRun) {
571
- printSummary("Build Failure Summary", await summarizeBuildFailure(String(error.output ?? "")));
572
- }
573
- throw error;
574
- }
575
- }
576
443
  function codexModel() {
577
444
  return process.env.CODEX_MODEL?.trim() || DEFAULT_CODEX_MODEL;
578
445
  }
579
446
  function claudeReviewModel() {
580
447
  return process.env.CLAUDE_REVIEW_MODEL?.trim() || DEFAULT_CLAUDE_REVIEW_MODEL;
581
448
  }
582
- function claudeSummaryModel() {
583
- return process.env.CLAUDE_SUMMARY_MODEL?.trim() || DEFAULT_CLAUDE_SUMMARY_MODEL;
584
- }
585
- function truncateText(text, maxChars = 12000) {
586
- return text.length <= maxChars ? text.trim() : text.trim().slice(-maxChars);
587
- }
588
- function fallbackBuildFailureSummary(output) {
589
- const lines = output
590
- .split(/\r?\n/)
591
- .map((line) => line.trim())
592
- .filter(Boolean);
593
- const tail = lines.length > 0 ? lines.slice(-8) : ["No build output captured."];
594
- return `Не удалось получить summary через Claude.\n\nПоследние строки лога:\n${tail.join("\n")}`;
595
- }
596
449
  async function summarizeBuildFailure(output) {
597
- if (!output.trim()) {
598
- return "Build verification failed, but no output was captured.";
599
- }
600
- let claudeCmd;
601
- try {
602
- claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
603
- }
604
- catch {
605
- return fallbackBuildFailureSummary(output);
606
- }
607
- const prompt = "Ниже лог упавшей build verification.\n" +
608
- "Сделай краткое резюме на русском языке, без воды.\n" +
609
- "Нужно обязательно выделить:\n" +
610
- "1. Где именно упало.\n" +
611
- "2. Главную причину падения.\n" +
612
- "3. Что нужно исправить дальше, если это очевидно.\n" +
613
- "Ответ дай максимум 5 короткими пунктами.\n\n" +
614
- `Лог:\n${truncateText(output)}`;
615
- printInfo(`Summarizing build failure with Claude (${claudeSummaryModel()})`);
616
- try {
617
- const summary = await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", prompt], {
618
- env: { ...process.env },
619
- dryRun: false,
620
- verbose: false,
621
- label: `claude:${claudeSummaryModel()}`,
622
- });
623
- return summary.trim() || fallbackBuildFailureSummary(output);
624
- }
625
- catch {
626
- return fallbackBuildFailureSummary(output);
627
- }
628
- }
629
- async function runClaudeSummary(claudeCmd, outputFile, prompt, verbose = false) {
630
- printInfo(`Preparing summary in ${outputFile}`);
631
- printPrompt("Claude", prompt);
632
- await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", "--allowedTools=Read,Write,Edit", prompt], {
633
- env: { ...process.env },
450
+ return summarizeBuildFailureViaPipeline(createPipelineContext({
451
+ issueKey: "build-failure-summary",
452
+ jiraRef: "build-failure-summary",
634
453
  dryRun: false,
635
- verbose,
636
- label: `claude:${claudeSummaryModel()}`,
637
- });
638
- requireArtifacts([outputFile], `Claude summary did not produce ${outputFile}.`);
639
- return readFileSync(outputFile, "utf8").trim();
640
- }
641
- async function summarizeTask(jiraRef) {
642
- const config = buildConfig("plan", jiraRef);
643
- const claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
644
- await fetchJiraIssue(config.jiraApiUrl, config.jiraTaskFile);
645
- const summaryPrompt = formatTemplate(TASK_SUMMARY_PROMPT_TEMPLATE, {
646
- jira_task_file: config.jiraTaskFile,
647
- task_summary_file: taskSummaryFile(config.taskKey),
648
- });
649
- const summaryText = await runClaudeSummary(claudeCmd, taskSummaryFile(config.taskKey), summaryPrompt);
650
- return { issueKey: config.jiraIssueKey, summaryText };
651
- }
652
- function resolveTaskIdentity(jiraRef) {
653
- const config = buildConfig("plan", jiraRef);
654
- const summaryPath = taskSummaryFile(config.taskKey);
655
- const summaryText = existsSync(summaryPath) ? readFileSync(summaryPath, "utf8").trim() : "";
656
- return { issueKey: config.jiraIssueKey, summaryText };
454
+ verbose: false,
455
+ runtime: runtimeServices,
456
+ }), output);
657
457
  }
658
458
  async function executeCommand(config, runFollowupVerify = true) {
659
459
  if (config.command === "auto") {
@@ -674,16 +474,10 @@ async function executeCommand(config, runFollowupVerify = true) {
674
474
  printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
675
475
  return false;
676
476
  }
677
- const { codexCmd, claudeCmd, dockerComposeCmd } = checkPrerequisites(config);
477
+ const { codexCmd, claudeCmd } = checkPrerequisites(config);
678
478
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
679
479
  process.env.JIRA_API_URL = config.jiraApiUrl;
680
480
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
681
- const planPrompt = formatPrompt(formatTemplate(PLAN_PROMPT_TEMPLATE, {
682
- jira_task_file: config.jiraTaskFile,
683
- design_file: designFile(config.taskKey),
684
- plan_file: planFile(config.taskKey),
685
- qa_file: qaFile(config.taskKey),
686
- }), config.extraPrompt);
687
481
  const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
688
482
  design_file: designFile(config.taskKey),
689
483
  plan_file: planFile(config.taskKey),
@@ -694,149 +488,149 @@ async function executeCommand(config, runFollowupVerify = true) {
694
488
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
695
489
  process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
696
490
  }
697
- await fetchJiraIssue(config.jiraApiUrl, config.jiraTaskFile);
698
- printInfo("Running Codex planning mode");
699
- printPrompt("Codex", planPrompt);
700
- await runCommand([codexCmd, "exec", "--model", codexModel(), "--full-auto", planPrompt], {
701
- env: { ...process.env },
491
+ await runPlanFlow(createPipelineContext({
492
+ issueKey: config.taskKey,
493
+ jiraRef: config.jiraRef,
702
494
  dryRun: config.dryRun,
703
495
  verbose: config.verbose,
704
- label: `codex:${codexModel()}`,
496
+ runtime: runtimeServices,
497
+ }), {
498
+ jiraApiUrl: config.jiraApiUrl,
499
+ jiraTaskFile: config.jiraTaskFile,
500
+ taskKey: config.taskKey,
501
+ codexCmd,
502
+ ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
705
503
  });
706
- requireArtifacts(planArtifacts(config.taskKey), "Plan mode did not produce the required artifacts.");
707
504
  return false;
708
505
  }
709
506
  if (config.command === "implement") {
710
507
  requireJiraTaskFile(config.jiraTaskFile);
711
508
  requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
712
- await runCodexInDocker(config, dockerComposeCmd, implementPrompt, "Running Codex implementation mode in isolated Docker");
713
- if (runFollowupVerify) {
714
- await runVerifyBuildInDocker(config, dockerComposeCmd, "Running build verification in isolated Docker");
715
- }
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
+ },
525
+ }
526
+ : {}),
527
+ });
716
528
  return false;
717
529
  }
718
530
  if (config.command === "review") {
719
531
  requireJiraTaskFile(config.jiraTaskFile);
720
- requireArtifacts(planArtifacts(config.taskKey), "Review mode requires plan artifacts from the planning phase.");
721
- const iteration = nextReviewIterationForTask(config.taskKey);
722
- const reviewFile = artifactFile("review", config.taskKey, iteration);
723
- const reviewReplyFile = artifactFile("review-reply", config.taskKey, iteration);
724
- const reviewSummaryFile = artifactFile("review-summary", config.taskKey, iteration);
725
- const reviewReplySummaryFile = artifactFile("review-reply-summary", config.taskKey, iteration);
726
- const claudePrompt = formatPrompt(formatTemplate(REVIEW_PROMPT_TEMPLATE, {
727
- jira_task_file: config.jiraTaskFile,
728
- design_file: designFile(config.taskKey),
729
- plan_file: planFile(config.taskKey),
730
- review_file: reviewFile,
731
- }), config.extraPrompt);
732
- const codexReplyPrompt = formatPrompt(formatTemplate(REVIEW_REPLY_PROMPT_TEMPLATE, {
733
- review_file: reviewFile,
734
- jira_task_file: config.jiraTaskFile,
735
- design_file: designFile(config.taskKey),
736
- plan_file: planFile(config.taskKey),
737
- review_reply_file: reviewReplyFile,
738
- }), config.extraPrompt);
739
- printInfo(`Running Claude review mode (iteration ${iteration})`);
740
- printPrompt("Claude", claudePrompt);
741
- await runCommand([
742
- claudeCmd,
743
- "--model",
744
- claudeReviewModel(),
745
- "-p",
746
- "--allowedTools=Read,Write,Edit",
747
- "--output-format",
748
- "stream-json",
749
- "--verbose",
750
- "--include-partial-messages",
751
- claudePrompt,
752
- ], {
753
- env: { ...process.env },
754
- dryRun: config.dryRun,
755
- verbose: config.verbose,
756
- label: `claude:${claudeReviewModel()}`,
757
- });
758
- if (!config.dryRun) {
759
- requireArtifacts([reviewFile], "Claude review did not produce the required review artifact.");
760
- const reviewSummaryText = await runClaudeSummary(claudeCmd, reviewSummaryFile, formatTemplate(REVIEW_SUMMARY_PROMPT_TEMPLATE, {
761
- review_file: reviewFile,
762
- review_summary_file: reviewSummaryFile,
763
- }), config.verbose);
764
- printSummary("Claude Comments", reviewSummaryText);
765
- }
766
- printInfo(`Running Codex review reply mode (iteration ${iteration})`);
767
- printPrompt("Codex", codexReplyPrompt);
768
- await runCommand([codexCmd, "exec", "--model", codexModel(), "--full-auto", codexReplyPrompt], {
769
- env: { ...process.env },
532
+ const result = await runReviewFlow(createPipelineContext({
533
+ issueKey: config.taskKey,
534
+ jiraRef: config.jiraRef,
770
535
  dryRun: config.dryRun,
771
536
  verbose: config.verbose,
772
- label: `codex:${codexModel()}`,
537
+ runtime: runtimeServices,
538
+ }), {
539
+ jiraTaskFile: config.jiraTaskFile,
540
+ taskKey: config.taskKey,
541
+ claudeCmd,
542
+ codexCmd,
543
+ ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
773
544
  });
774
- let readyToMerge = false;
775
- if (!config.dryRun) {
776
- requireArtifacts([reviewReplyFile], "Codex review reply did not produce the required review-reply artifact.");
777
- const reviewReplySummaryText = await runClaudeSummary(claudeCmd, reviewReplySummaryFile, formatTemplate(REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, {
778
- review_reply_file: reviewReplyFile,
779
- review_reply_summary_file: reviewReplySummaryFile,
780
- }), config.verbose);
781
- printSummary("Codex Reply Summary", reviewReplySummaryText);
782
- if (existsSync(READY_TO_MERGE_FILE)) {
783
- printPanel("Ready To Merge", "Изменения готовы к merge\nФайл ready-to-merge.md создан.", "green");
784
- readyToMerge = true;
785
- }
786
- }
787
- return readyToMerge;
545
+ return result.readyToMerge;
788
546
  }
789
547
  if (config.command === "review-fix") {
790
548
  requireJiraTaskFile(config.jiraTaskFile);
791
- requireArtifacts(planArtifacts(config.taskKey), "Review-fix mode requires plan artifacts from the planning phase.");
792
- const latestIteration = latestReviewReplyIteration(config.taskKey);
793
- if (latestIteration === null) {
794
- throw new TaskRunnerError(`Review-fix mode requires at least one review-reply-${config.taskKey}-N.md artifact.`);
795
- }
796
- const reviewReplyFile = artifactFile("review-reply", config.taskKey, latestIteration);
797
- const reviewFixFile = artifactFile("review-fix", config.taskKey, latestIteration);
798
- const reviewFixPrompt = formatPrompt(formatTemplate(REVIEW_FIX_PROMPT_TEMPLATE, {
799
- review_reply_file: reviewReplyFile,
800
- items: config.reviewFixPoints ?? "",
801
- review_fix_file: reviewFixFile,
802
- }), config.extraPrompt);
803
- await runCodexInDocker(config, dockerComposeCmd, reviewFixPrompt, `Running Codex review-fix mode in isolated Docker (iteration ${latestIteration})`);
804
- if (!config.dryRun) {
805
- requireArtifacts([reviewFixFile], "Review-fix mode did not produce the required review-fix artifact.");
806
- }
807
- if (runFollowupVerify) {
808
- await runVerifyBuildInDocker(config, dockerComposeCmd, "Running build verification in isolated Docker");
809
- }
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
+ },
568
+ }
569
+ : {}),
570
+ });
810
571
  return false;
811
572
  }
812
573
  if (config.command === "test") {
813
574
  requireJiraTaskFile(config.jiraTaskFile);
814
- requireArtifacts(planArtifacts(config.taskKey), "Test mode requires plan artifacts from the planning phase.");
815
- await runVerifyBuildInDocker(config, dockerComposeCmd, "Running build verification in isolated Docker");
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
+ },
590
+ }
591
+ : {}),
592
+ });
816
593
  return false;
817
594
  }
818
595
  if (config.command === "test-fix" || config.command === "test-linter-fix") {
819
596
  requireJiraTaskFile(config.jiraTaskFile);
820
- requireArtifacts(planArtifacts(config.taskKey), `${config.command} mode requires plan artifacts from the planning phase.`);
821
- const prompt = formatPrompt(config.command === "test-fix" ? TEST_FIX_PROMPT_TEMPLATE : TEST_LINTER_FIX_PROMPT_TEMPLATE, config.extraPrompt);
822
- await runCodexInDocker(config, dockerComposeCmd, prompt, `Running Codex ${config.command} mode in isolated Docker`);
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,
605
+ taskKey: config.taskKey,
606
+ dockerComposeFile: config.dockerComposeFile,
607
+ ...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
608
+ });
823
609
  return false;
824
610
  }
825
611
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
826
612
  }
827
613
  async function runAutoPipelineDryRun(config) {
614
+ const { codexCmd, claudeCmd } = checkPrerequisites(config);
828
615
  printInfo("Dry-run auto pipeline: plan -> implement -> test -> review/review-fix/test");
829
- await executeCommand(buildPhaseConfig(config, "plan"));
830
- await executeCommand(buildPhaseConfig(config, "implement"), false);
831
- await executeCommand(buildPhaseConfig(config, "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);
832
620
  for (let iteration = 1; iteration <= AUTO_MAX_REVIEW_ITERATIONS; iteration += 1) {
833
621
  printInfo(`Dry-run auto review iteration ${iteration}/${AUTO_MAX_REVIEW_ITERATIONS}`);
834
- await executeCommand(buildPhaseConfig(config, "review"));
835
- await executeCommand({
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}`);
627
+ }
628
+ await runAutoStepViaFlow(buildPhaseConfig(config, "review"), reviewStep, codexCmd, claudeCmd, dryState);
629
+ await runAutoStepViaFlow({
836
630
  ...buildPhaseConfig(config, "review-fix"),
837
631
  extraPrompt: appendPromptText(config.extraPrompt, AUTO_REVIEW_FIX_EXTRA_PROMPT),
838
- }, false);
839
- await executeCommand(buildPhaseConfig(config, "test"));
632
+ }, reviewFixStep, codexCmd, claudeCmd, dryState);
633
+ await runAutoStepViaFlow(buildPhaseConfig(config, "test"), testStep, codexCmd, claudeCmd, dryState);
840
634
  }
841
635
  }
842
636
  async function runAutoPipeline(config) {
@@ -844,6 +638,7 @@ async function runAutoPipeline(config) {
844
638
  await runAutoPipelineDryRun(config);
845
639
  return;
846
640
  }
641
+ const { codexCmd, claudeCmd } = checkPrerequisites(config);
847
642
  let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
848
643
  if (config.autoFromPhase) {
849
644
  rewindAutoPipelineState(state, config.autoFromPhase);
@@ -887,7 +682,7 @@ async function runAutoPipeline(config) {
887
682
  saveAutoPipelineState(state);
888
683
  try {
889
684
  printInfo(`Running auto step: ${step.id}`);
890
- const readyToMerge = await executeCommand(configForAutoStep(config, step), !["implement", "review-fix"].includes(step.command));
685
+ const readyToMerge = await runAutoStepViaFlow(configForAutoStep(config, step), step, codexCmd, claudeCmd, state);
891
686
  step.status = "done";
892
687
  step.finishedAt = nowIso8601();
893
688
  step.returnCode = 0;
@@ -1098,7 +893,6 @@ async function ensureHistoryFile() {
1098
893
  async function runInteractive(jiraRef, forceRefresh = false) {
1099
894
  const config = buildConfig("plan", jiraRef);
1100
895
  const jiraTaskPath = config.jiraTaskFile;
1101
- const taskIdentity = forceRefresh || !existsSync(jiraTaskPath) ? await summarizeTask(jiraRef) : resolveTaskIdentity(jiraRef);
1102
896
  await ensureHistoryFile();
1103
897
  const historyLines = (await readFile(HISTORY_FILE, "utf8"))
1104
898
  .split(/\r?\n/)
@@ -1120,8 +914,8 @@ async function runInteractive(jiraRef, forceRefresh = false) {
1120
914
  "/exit",
1121
915
  ];
1122
916
  const ui = new InteractiveUi({
1123
- issueKey: taskIdentity.issueKey,
1124
- summaryText: taskIdentity.summaryText || "Task summary is not available yet. Run /plan or refresh Jira data.",
917
+ issueKey: config.jiraIssueKey,
918
+ summaryText: "Starting interactive session...",
1125
919
  cwd: process.cwd(),
1126
920
  commands: commandList,
1127
921
  onSubmit: async (line) => {
@@ -1159,14 +953,40 @@ async function runInteractive(jiraRef, forceRefresh = false) {
1159
953
  },
1160
954
  }, historyLines);
1161
955
  ui.mount();
1162
- printInfo(`Interactive mode for ${taskIdentity.issueKey}`);
1163
- if (taskIdentity.summaryText) {
1164
- printInfo("Task summary loaded.");
956
+ printInfo(`Interactive mode for ${config.jiraIssueKey}`);
957
+ printInfo("Use /help to see commands.");
958
+ try {
959
+ ui.setBusy(true, "preflight");
960
+ await runPreflightFlow(createPipelineContext({
961
+ issueKey: config.taskKey,
962
+ jiraRef: config.jiraRef,
963
+ dryRun: false,
964
+ verbose: config.verbose,
965
+ runtime: runtimeServices,
966
+ setSummary: (markdown) => {
967
+ ui.setSummary(markdown);
968
+ },
969
+ }), {
970
+ jiraApiUrl: config.jiraApiUrl,
971
+ jiraTaskFile: config.jiraTaskFile,
972
+ taskKey: config.taskKey,
973
+ forceRefresh,
974
+ });
975
+ if (!existsSync(taskSummaryFile(config.taskKey))) {
976
+ ui.setSummary("Task summary is not available yet. Run `/plan` or refresh Jira data.");
977
+ }
1165
978
  }
1166
- else {
1167
- printInfo("Task summary is not available yet.");
979
+ catch (error) {
980
+ if (error instanceof TaskRunnerError) {
981
+ printError(error.message);
982
+ }
983
+ else {
984
+ throw error;
985
+ }
986
+ }
987
+ finally {
988
+ ui.setBusy(false);
1168
989
  }
1169
- printInfo("Use /help to see commands.");
1170
990
  return await new Promise((resolve, reject) => {
1171
991
  const interval = setInterval(() => {
1172
992
  if (!exiting) {