agentweaver 0.1.5 → 0.1.7

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 (56) hide show
  1. package/Dockerfile.codex +56 -0
  2. package/README.md +38 -15
  3. package/dist/artifacts.js +38 -8
  4. package/dist/executors/configs/fetch-gitlab-review-config.js +3 -0
  5. package/dist/executors/fetch-gitlab-review-executor.js +25 -0
  6. package/dist/flow-state.js +134 -0
  7. package/dist/gitlab.js +153 -0
  8. package/dist/index.js +397 -250
  9. package/dist/interactive-ui.js +170 -42
  10. package/dist/pipeline/declarative-flow-runner.js +28 -0
  11. package/dist/pipeline/flow-specs/auto.json +530 -392
  12. package/dist/pipeline/flow-specs/bug-analyze.json +149 -0
  13. package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
  14. package/dist/pipeline/flow-specs/implement.json +0 -9
  15. package/dist/pipeline/flow-specs/plan.json +133 -0
  16. package/dist/pipeline/flow-specs/review-fix.json +2 -11
  17. package/dist/pipeline/flow-specs/review-project.json +243 -0
  18. package/dist/pipeline/flow-specs/run-go-linter-loop.json +155 -0
  19. package/dist/pipeline/flow-specs/run-go-tests-loop.json +155 -0
  20. package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
  21. package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
  22. package/dist/pipeline/flow-specs/task-describe.json +25 -0
  23. package/dist/pipeline/node-registry.js +28 -1
  24. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
  25. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
  26. package/dist/pipeline/nodes/jira-issue-check-node.js +53 -0
  27. package/dist/pipeline/nodes/local-script-check-node.js +81 -0
  28. package/dist/pipeline/nodes/review-findings-form-node.js +14 -14
  29. package/dist/pipeline/prompt-registry.js +5 -5
  30. package/dist/pipeline/registry.js +2 -0
  31. package/dist/pipeline/value-resolver.js +7 -1
  32. package/dist/prompts.js +11 -4
  33. package/dist/scope.js +118 -0
  34. package/dist/structured-artifacts.js +33 -0
  35. package/docker-compose.yml +445 -0
  36. package/package.json +8 -3
  37. package/run_go_coverage.sh +113 -0
  38. package/run_go_linter.sh +89 -0
  39. package/run_go_tests.sh +83 -0
  40. package/verify_build.sh +105 -0
  41. package/dist/executors/claude-summary-executor.js +0 -31
  42. package/dist/executors/configs/claude-summary-config.js +0 -8
  43. package/dist/pipeline/flow-runner.js +0 -13
  44. package/dist/pipeline/flow-specs/test-fix.json +0 -24
  45. package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
  46. package/dist/pipeline/flow-specs/test.json +0 -19
  47. package/dist/pipeline/flow-types.js +0 -1
  48. package/dist/pipeline/flows/implement-flow.js +0 -47
  49. package/dist/pipeline/flows/plan-flow.js +0 -42
  50. package/dist/pipeline/flows/review-fix-flow.js +0 -62
  51. package/dist/pipeline/flows/review-flow.js +0 -124
  52. package/dist/pipeline/flows/test-fix-flow.js +0 -12
  53. package/dist/pipeline/flows/test-flow.js +0 -32
  54. package/dist/pipeline/nodes/claude-summary-node.js +0 -38
  55. package/dist/pipeline/nodes/implement-codex-node.js +0 -16
  56. package/dist/pipeline/nodes/task-summary-node.js +0 -42
package/dist/index.js CHANGED
@@ -3,36 +3,35 @@ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "no
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
- import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, ensureTaskWorkspaceDir, jiraTaskFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, taskWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
6
+ import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
7
7
  import { TaskRunnerError } from "./errors.js";
8
- import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
8
+ import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
9
+ import { requireJiraTaskFile } from "./jira.js";
9
10
  import { validateStructuredArtifacts } from "./structured-artifacts.js";
10
11
  import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
11
12
  import { createPipelineContext } from "./pipeline/context.js";
12
13
  import { loadAutoFlow } from "./pipeline/auto-flow.js";
13
14
  import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
14
15
  import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
15
- import { runPreflightFlow } from "./pipeline/flows/preflight-flow.js";
16
16
  import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
17
- import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
17
+ import { agentweaverHome, defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
18
18
  import { runCommand } from "./runtime/process-runner.js";
19
19
  import { InteractiveUi } from "./interactive-ui.js";
20
20
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
21
21
  import { requestUserInputInTerminal } from "./user-input.js";
22
+ import { detectGitBranchName, requestTaskScope, resolveProjectScope, resolveTaskScope, } from "./scope.js";
22
23
  const COMMANDS = [
23
24
  "bug-analyze",
24
25
  "bug-fix",
26
+ "gitlab-review",
25
27
  "mr-description",
26
28
  "plan",
27
29
  "task-describe",
28
30
  "implement",
29
31
  "review",
30
32
  "review-fix",
31
- "test",
32
- "test-fix",
33
- "test-linter-fix",
34
- "run-tests-loop",
35
- "run-linter-loop",
33
+ "run-go-tests-loop",
34
+ "run-go-linter-loop",
36
35
  "auto",
37
36
  "auto-status",
38
37
  "auto-reset",
@@ -79,36 +78,37 @@ function formatProcessFailure(error) {
79
78
  }
80
79
  function usage() {
81
80
  return `Usage:
81
+ agentweaver
82
82
  agentweaver <jira-browse-url|jira-issue-key>
83
83
  agentweaver --force <jira-browse-url|jira-issue-key>
84
+ agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>] <jira-browse-url|jira-issue-key>
84
85
  agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
85
86
  agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
86
87
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
87
- agentweaver plan [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
88
+ agentweaver plan [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
88
89
  agentweaver task-describe [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
89
- agentweaver implement [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
90
- agentweaver review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
91
- agentweaver review-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
92
- agentweaver test [--dry] [--verbose] <jira-browse-url|jira-issue-key>
93
- agentweaver test-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
94
- agentweaver test-linter-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
95
- agentweaver run-tests-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
96
- agentweaver run-linter-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
97
- agentweaver auto [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
98
- agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> <jira-browse-url|jira-issue-key>
90
+ agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
91
+ agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
92
+ agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
93
+ agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
94
+ agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
95
+ agentweaver auto [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
96
+ agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
99
97
  agentweaver auto --help-phases
100
- agentweaver auto-status <jira-browse-url|jira-issue-key>
101
- agentweaver auto-reset <jira-browse-url|jira-issue-key>
98
+ agentweaver auto-status [<jira-browse-url|jira-issue-key>]
99
+ agentweaver auto-reset [<jira-browse-url|jira-issue-key>]
102
100
 
103
101
  Interactive Mode:
104
- When started with only a Jira task, the script opens an interactive UI.
102
+ When started without a command, the script opens an interactive UI.
103
+ If a Jira task is provided, interactive mode starts in task scope; otherwise it starts in project scope.
105
104
  Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
106
105
 
107
106
  Flags:
108
107
  --version Show package version
109
- --force In interactive mode, force refresh Jira task and task summary
108
+ --force In interactive mode, regenerate task summary in Jira-backed flows
110
109
  --dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
111
110
  --verbose Show live stdout/stderr of launched commands
111
+ --scope Explicit workflow scope name for non-Jira runs
112
112
  --prompt Extra prompt text appended to the base prompt
113
113
 
114
114
  Required environment variables:
@@ -116,12 +116,17 @@ Required environment variables:
116
116
 
117
117
  Optional environment variables:
118
118
  JIRA_BASE_URL
119
+ GITLAB_TOKEN
119
120
  AGENTWEAVER_HOME
120
121
  DOCKER_COMPOSE_BIN
121
122
  CODEX_BIN
122
123
  CODEX_MODEL
123
124
  CLAUDE_BIN
124
- CLAUDE_MODEL`;
125
+ CLAUDE_MODEL
126
+
127
+ Notes:
128
+ - Task-only flows will ask for Jira task via user-input when it is not passed as an argument.
129
+ - Scope-flexible flows use the current git branch by default when Jira task is not provided.`;
125
130
  }
126
131
  function packageVersion() {
127
132
  const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
@@ -173,28 +178,6 @@ function createAutoPipelineState(config) {
173
178
  },
174
179
  };
175
180
  }
176
- function stripExecutionStatePayload(executionState) {
177
- return {
178
- flowKind: executionState.flowKind,
179
- flowVersion: executionState.flowVersion,
180
- terminated: executionState.terminated,
181
- ...(executionState.terminationReason ? { terminationReason: executionState.terminationReason } : {}),
182
- phases: executionState.phases.map((phase) => ({
183
- id: phase.id,
184
- status: phase.status,
185
- repeatVars: { ...phase.repeatVars },
186
- ...(phase.startedAt ? { startedAt: phase.startedAt } : {}),
187
- ...(phase.finishedAt ? { finishedAt: phase.finishedAt } : {}),
188
- steps: phase.steps.map((step) => ({
189
- id: step.id,
190
- status: step.status,
191
- ...(step.startedAt ? { startedAt: step.startedAt } : {}),
192
- ...(step.finishedAt ? { finishedAt: step.finishedAt } : {}),
193
- ...(step.stopFlow !== undefined ? { stopFlow: step.stopFlow } : {}),
194
- })),
195
- })),
196
- };
197
- }
198
181
  function loadAutoPipelineState(config) {
199
182
  const filePath = autoStateFile(config.taskKey);
200
183
  if (!existsSync(filePath)) {
@@ -228,7 +211,7 @@ function loadAutoPipelineState(config) {
228
211
  }
229
212
  function saveAutoPipelineState(state) {
230
213
  state.updatedAt = nowIso8601();
231
- ensureTaskWorkspaceDir(state.issueKey);
214
+ ensureScopeWorkspaceDir(state.issueKey);
232
215
  writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify({
233
216
  ...state,
234
217
  executionState: stripExecutionStatePayload(state.executionState),
@@ -327,6 +310,30 @@ function syncAutoStepsFromExecutionState(state) {
327
310
  state.currentStep = findCurrentExecutionStep(state);
328
311
  state.status = deriveAutoPipelineStatus(state);
329
312
  }
313
+ function buildAutoResumeDetails(state) {
314
+ const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
315
+ const lines = [
316
+ "Interrupted auto run found.",
317
+ `Current step: ${currentStep}`,
318
+ `Updated: ${state.updatedAt}`,
319
+ ];
320
+ if (state.lastError) {
321
+ lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
322
+ }
323
+ return lines.join("\n");
324
+ }
325
+ function buildFlowResumeDetails(state) {
326
+ const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
327
+ const lines = [
328
+ "Interrupted run found.",
329
+ `Current step: ${currentStep}`,
330
+ `Updated: ${state.updatedAt}`,
331
+ ];
332
+ if (state.lastError) {
333
+ lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
334
+ }
335
+ return lines.join("\n");
336
+ }
330
337
  function printAutoPhasesHelp() {
331
338
  const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
332
339
  phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
@@ -362,7 +369,7 @@ function loadEnvFile(envFilePath) {
362
369
  }
363
370
  function nextReviewIterationForTask(taskKey) {
364
371
  let maxIndex = 0;
365
- const workspaceDir = taskWorkspaceDir(taskKey);
372
+ const workspaceDir = scopeWorkspaceDir(taskKey);
366
373
  if (!existsSync(workspaceDir)) {
367
374
  return 1;
368
375
  }
@@ -379,7 +386,7 @@ function nextReviewIterationForTask(taskKey) {
379
386
  }
380
387
  function latestReviewReplyIteration(taskKey) {
381
388
  let maxIndex = null;
382
- const workspaceDir = taskWorkspaceDir(taskKey);
389
+ const workspaceDir = scopeWorkspaceDir(taskKey);
383
390
  if (!existsSync(workspaceDir)) {
384
391
  return null;
385
392
  }
@@ -395,23 +402,81 @@ function latestReviewReplyIteration(taskKey) {
395
402
  }
396
403
  return maxIndex;
397
404
  }
398
- function buildConfig(command, jiraRef, options = {}) {
399
- const jiraIssueKey = extractIssueKey(jiraRef);
400
- ensureTaskWorkspaceDir(jiraIssueKey);
405
+ function buildBaseConfig(command, options = {}) {
406
+ const homeDir = agentweaverHome(PACKAGE_ROOT);
401
407
  return {
402
408
  command,
403
- jiraRef,
409
+ jiraRef: options.jiraRef ?? null,
410
+ scopeName: options.scopeName ?? null,
404
411
  reviewFixPoints: options.reviewFixPoints ?? null,
405
412
  extraPrompt: options.extraPrompt ?? null,
406
413
  autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
407
414
  dryRun: options.dryRun ?? false,
408
415
  verbose: options.verbose ?? false,
409
416
  dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
410
- jiraIssueKey,
411
- taskKey: jiraIssueKey,
412
- jiraBrowseUrl: buildJiraBrowseUrl(jiraRef),
413
- jiraApiUrl: buildJiraApiUrl(jiraRef),
414
- jiraTaskFile: jiraTaskFile(jiraIssueKey),
417
+ runGoTestsScript: path.join(homeDir, "run_go_tests.sh"),
418
+ runGoLinterScript: path.join(homeDir, "run_go_linter.sh"),
419
+ runGoCoverageScript: path.join(homeDir, "run_go_coverage.sh"),
420
+ };
421
+ }
422
+ function commandRequiresTask(command) {
423
+ return (command === "plan" ||
424
+ command === "bug-analyze" ||
425
+ command === "bug-fix" ||
426
+ command === "gitlab-review" ||
427
+ command === "mr-description" ||
428
+ command === "task-describe" ||
429
+ command === "auto" ||
430
+ command === "auto-status" ||
431
+ command === "auto-reset");
432
+ }
433
+ function commandSupportsProjectScope(command) {
434
+ return (command === "implement" ||
435
+ command === "review" ||
436
+ command === "review-fix" ||
437
+ command === "run-go-tests-loop" ||
438
+ command === "run-go-linter-loop");
439
+ }
440
+ async function resolveScopeForCommand(config, requestUserInput) {
441
+ if (config.jiraRef?.trim()) {
442
+ return resolveTaskScope(config.jiraRef, config.scopeName);
443
+ }
444
+ if (commandRequiresTask(config.command)) {
445
+ try {
446
+ const taskScope = await requestTaskScope(requestUserInput);
447
+ return config.scopeName ? resolveTaskScope(taskScope.jiraRef, config.scopeName) : taskScope;
448
+ }
449
+ catch (error) {
450
+ if (error instanceof TaskRunnerError && error.message.includes("no TTY is available")) {
451
+ throw new TaskRunnerError(`Command '${config.command}' requires a Jira task.\n` +
452
+ "Pass Jira issue key / browse URL as an argument, or run the command in an interactive terminal.");
453
+ }
454
+ throw error;
455
+ }
456
+ }
457
+ if (commandSupportsProjectScope(config.command)) {
458
+ return resolveProjectScope(config.scopeName);
459
+ }
460
+ throw new TaskRunnerError(`Unsupported scope policy for command: ${config.command}`);
461
+ }
462
+ function buildRuntimeConfig(baseConfig, scope) {
463
+ ensureScopeWorkspaceDir(scope.scopeKey);
464
+ if (scope.scopeType === "task") {
465
+ return {
466
+ ...baseConfig,
467
+ scope,
468
+ taskKey: scope.scopeKey,
469
+ jiraRef: scope.jiraRef,
470
+ jiraBrowseUrl: scope.jiraBrowseUrl,
471
+ jiraApiUrl: scope.jiraApiUrl,
472
+ jiraTaskFile: scope.jiraTaskFile,
473
+ };
474
+ }
475
+ return {
476
+ ...baseConfig,
477
+ scope,
478
+ taskKey: scope.scopeKey,
479
+ jiraRef: scope.scopeKey,
415
480
  };
416
481
  }
417
482
  function checkPrerequisites(config) {
@@ -421,40 +486,35 @@ function checkPrerequisites(config) {
421
486
  config.command === "plan" ||
422
487
  config.command === "task-describe" ||
423
488
  config.command === "review" ||
424
- config.command === "run-tests-loop" ||
425
- config.command === "run-linter-loop") {
489
+ config.command === "run-go-tests-loop" ||
490
+ config.command === "run-go-linter-loop") {
426
491
  resolveCmd("codex", "CODEX_BIN");
427
492
  }
428
493
  if (config.command === "review") {
429
494
  resolveCmd("claude", "CLAUDE_BIN");
430
495
  }
431
- if (["implement", "review-fix", "test", "run-tests-loop", "run-linter-loop"].includes(config.command)) {
432
- resolveDockerComposeCmd();
433
- if (!existsSync(config.dockerComposeFile)) {
434
- throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
435
- }
436
- }
437
496
  }
438
497
  function checkAutoPrerequisites(config) {
439
498
  resolveCmd("codex", "CODEX_BIN");
440
499
  resolveCmd("claude", "CLAUDE_BIN");
441
- resolveDockerComposeCmd();
442
- if (!existsSync(config.dockerComposeFile)) {
443
- throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
444
- }
445
500
  }
446
- function autoFlowParams(config) {
501
+ function autoFlowParams(config, forceRefreshSummary = false) {
447
502
  return {
448
503
  jiraApiUrl: config.jiraApiUrl,
449
504
  taskKey: config.taskKey,
450
505
  dockerComposeFile: config.dockerComposeFile,
506
+ runGoTestsScript: config.runGoTestsScript,
507
+ runGoLinterScript: config.runGoLinterScript,
508
+ runGoCoverageScript: config.runGoCoverageScript,
451
509
  extraPrompt: config.extraPrompt,
452
510
  reviewFixPoints: config.reviewFixPoints,
511
+ forceRefresh: forceRefreshSummary,
453
512
  };
454
513
  }
455
514
  const FLOW_DESCRIPTIONS = {
456
515
  auto: "Полный пайплайн задачи: планирование, реализация, проверки, ревью, ответы на ревью и повторные итерации до готовности к merge.",
457
516
  "bug-analyze": "Анализирует баг по Jira и создаёт структурированные артефакты: гипотезу причины, дизайн исправления и план работ.",
517
+ "gitlab-review": "Запрашивает GitLab MR URL через user-input, загружает комментарии код-ревью по API и сохраняет markdown плюс structured JSON artifact.",
458
518
  "bug-fix": "Берёт результаты bug-analyze как source of truth и реализует исправление бага в коде.",
459
519
  "mr-description": "Готовит краткое intent-описание для merge request на основе задачи и текущих изменений.",
460
520
  plan: "Загружает задачу из Jira и создаёт дизайн, план реализации и QA-план в structured JSON и markdown.",
@@ -462,11 +522,8 @@ const FLOW_DESCRIPTIONS = {
462
522
  implement: "Реализует задачу по утверждённым design/plan артефактам и при необходимости запускает post-verify сборки.",
463
523
  review: "Запускает Claude-код-ревью текущих изменений, валидирует structured findings, затем готовит ответ на замечания через Codex.",
464
524
  "review-fix": "Исправляет замечания после review-reply, обновляет код и прогоняет обязательные проверки после правок.",
465
- test: "Запускает verify/build-проверку в контейнере и показывает результат с краткой сводкой при падении.",
466
- "test-fix": "Прогоняет тесты, исправляет найденные проблемы и готовит код к следующей успешной проверке.",
467
- "test-linter-fix": "Прогоняет линтер и генерацию, затем исправляет замечания для чистого прогона.",
468
- "run-tests-loop": "Циклически запускает `./run_tests.sh`, анализирует последнюю ошибку и правит код до успешного прохождения или исчерпания попыток.",
469
- "run-linter-loop": "Циклически запускает `./run_linter.sh`, исправляет проблемы линтера или генерации и повторяет попытки до успеха.",
525
+ "run-go-tests-loop": "Циклически запускает `./run_go_tests.sh` локально, анализирует последнюю ошибку и правит код до успешного прохождения или исчерпания попыток.",
526
+ "run-go-linter-loop": "Циклически запускает `./run_go_linter.sh` локально, исправляет проблемы линтера или генерации и повторяет попытки до успеха.",
470
527
  };
471
528
  function flowDescription(id) {
472
529
  return FLOW_DESCRIPTIONS[id] ?? "Описание для этого flow пока не задано.";
@@ -506,64 +563,139 @@ function interactiveFlowDefinitions() {
506
563
  autoFlowDefinition(),
507
564
  declarativeFlowDefinition("bug-analyze", "bug-analyze", "bug-analyze.json"),
508
565
  declarativeFlowDefinition("bug-fix", "bug-fix", "bug-fix.json"),
566
+ declarativeFlowDefinition("gitlab-review", "gitlab-review", "gitlab-review.json"),
509
567
  declarativeFlowDefinition("mr-description", "mr-description", "mr-description.json"),
510
568
  declarativeFlowDefinition("plan", "plan", "plan.json"),
511
569
  declarativeFlowDefinition("task-describe", "task-describe", "task-describe.json"),
512
570
  declarativeFlowDefinition("implement", "implement", "implement.json"),
513
571
  declarativeFlowDefinition("review", "review", "review.json"),
514
572
  declarativeFlowDefinition("review-fix", "review-fix", "review-fix.json"),
515
- declarativeFlowDefinition("test", "test", "test.json"),
516
- declarativeFlowDefinition("test-fix", "test-fix", "test-fix.json"),
517
- declarativeFlowDefinition("test-linter-fix", "test-linter-fix", "test-linter-fix.json"),
518
- declarativeFlowDefinition("run-tests-loop", "run-tests-loop", "run-tests-loop.json"),
519
- declarativeFlowDefinition("run-linter-loop", "run-linter-loop", "run-linter-loop.json"),
573
+ declarativeFlowDefinition("run-go-tests-loop", "run-go-tests-loop", "run-go-tests-loop.json"),
574
+ declarativeFlowDefinition("run-go-linter-loop", "run-go-linter-loop", "run-go-linter-loop.json"),
520
575
  ];
521
576
  }
522
577
  function publishFlowState(flowId, executionState) {
523
578
  setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
524
579
  }
525
- async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal) {
580
+ function loadTaskSummaryMarkdown(taskKey) {
581
+ const summaryPath = taskSummaryFile(taskKey);
582
+ if (!existsSync(summaryPath)) {
583
+ return null;
584
+ }
585
+ const markdown = readFileSync(summaryPath, "utf8").trim();
586
+ return markdown.length > 0 ? markdown : null;
587
+ }
588
+ function syncInteractiveTaskSummary(ui, scope, forceRefresh = false) {
589
+ if (scope.scopeType !== "task" || forceRefresh) {
590
+ ui.clearSummary();
591
+ return;
592
+ }
593
+ const summaryMarkdown = loadTaskSummaryMarkdown(scope.scopeKey);
594
+ if (summaryMarkdown) {
595
+ ui.setSummary(summaryMarkdown);
596
+ return;
597
+ }
598
+ ui.clearSummary();
599
+ }
600
+ function findCurrentFlowExecutionStep(state) {
601
+ for (const phase of state.executionState.phases) {
602
+ const runningStep = phase.steps.find((step) => step.status === "running");
603
+ if (runningStep) {
604
+ return `${phase.id}:${runningStep.id}`;
605
+ }
606
+ const pendingStep = phase.steps.find((step) => step.status === "pending");
607
+ if (pendingStep && phase.steps.some((step) => step.status === "done" || step.status === "skipped")) {
608
+ return `${phase.id}:${pendingStep.id}`;
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
526
614
  const context = createPipelineContext({
527
615
  issueKey: config.taskKey,
528
616
  jiraRef: config.jiraRef,
529
617
  dryRun: config.dryRun,
530
618
  verbose: config.verbose,
531
619
  runtime: runtimeServices,
620
+ ...(setSummary ? { setSummary } : {}),
532
621
  requestUserInput,
533
622
  });
534
623
  const flow = loadDeclarativeFlow(fileName);
535
- const executionState = {
624
+ const initialExecutionState = {
536
625
  flowKind: flow.kind,
537
626
  flowVersion: flow.version,
538
627
  terminated: false,
539
628
  phases: [],
540
629
  };
630
+ let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, config.command) : null;
631
+ if (persistedState && launchMode === "resume") {
632
+ persistedState = prepareFlowStateForResume(persistedState);
633
+ }
634
+ else if (launchMode === "restart") {
635
+ resetFlowRunState(config.scope.scopeKey, config.command);
636
+ }
637
+ const executionState = persistedState?.executionState ?? initialExecutionState;
638
+ const state = persistedState ?? createFlowRunState(config.scope.scopeKey, config.command, executionState);
639
+ state.status = "running";
640
+ state.lastError = null;
641
+ state.currentStep = findCurrentFlowExecutionStep(state);
642
+ state.executionState = executionState;
643
+ saveFlowRunState(state);
541
644
  publishFlowState(config.command, executionState);
542
- for (const phase of flow.phases) {
543
- await runExpandedPhase(phase, context, flowParams, flow.constants, {
544
- executionState,
545
- flowKind: flow.kind,
546
- flowVersion: flow.version,
547
- onStateChange: async (state) => {
548
- publishFlowState(config.command, state);
549
- },
550
- });
645
+ try {
646
+ for (const phase of flow.phases) {
647
+ await runExpandedPhase(phase, context, flowParams, flow.constants, {
648
+ executionState,
649
+ flowKind: flow.kind,
650
+ flowVersion: flow.version,
651
+ onStateChange: async (nextExecutionState) => {
652
+ state.executionState = nextExecutionState;
653
+ state.currentStep = findCurrentFlowExecutionStep(state);
654
+ saveFlowRunState(state);
655
+ publishFlowState(config.command, nextExecutionState);
656
+ },
657
+ onStepStart: async (currentPhase, step) => {
658
+ state.currentStep = `${currentPhase.id}:${step.id}`;
659
+ saveFlowRunState(state);
660
+ },
661
+ });
662
+ }
663
+ state.status = "completed";
664
+ state.currentStep = null;
665
+ state.lastError = null;
666
+ state.executionState = executionState;
667
+ saveFlowRunState(state);
668
+ }
669
+ catch (error) {
670
+ state.status = "blocked";
671
+ state.currentStep = findCurrentFlowExecutionStep(state);
672
+ state.lastError = {
673
+ returnCode: Number(error.returnCode ?? 1),
674
+ message: error.message || "command failed",
675
+ };
676
+ if (state.currentStep) {
677
+ state.lastError.step = state.currentStep;
678
+ }
679
+ state.executionState = executionState;
680
+ saveFlowRunState(state);
681
+ throw error;
551
682
  }
552
683
  }
553
- async function runAutoPhaseViaSpec(config, phaseId, executionState, state) {
684
+ async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false) {
554
685
  const context = createPipelineContext({
555
686
  issueKey: config.taskKey,
556
687
  jiraRef: config.jiraRef,
557
688
  dryRun: config.dryRun,
558
689
  verbose: config.verbose,
559
690
  runtime: runtimeServices,
691
+ ...(setSummary ? { setSummary } : {}),
560
692
  requestUserInput: requestUserInputInTerminal,
561
693
  });
562
694
  const autoFlow = loadAutoFlow();
563
695
  const phase = findPhaseById(autoFlow.phases, phaseId);
564
696
  publishFlowState("auto", executionState);
565
697
  try {
566
- const result = await runExpandedPhase(phase, context, autoFlowParams(config), autoFlow.constants, {
698
+ const result = await runExpandedPhase(phase, context, autoFlowParams(config, forceRefreshSummary), autoFlow.constants, {
567
699
  executionState,
568
700
  flowKind: autoFlow.kind,
569
701
  flowVersion: autoFlow.version,
@@ -635,9 +767,18 @@ async function summarizeBuildFailure(output) {
635
767
  requestUserInput: requestUserInputInTerminal,
636
768
  }), output);
637
769
  }
638
- async function executeCommand(config, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal) {
770
+ function requireTaskScopeConfig(config) {
771
+ if (config.scope.scopeType !== "task" || !config.jiraBrowseUrl || !config.jiraApiUrl || !config.jiraTaskFile) {
772
+ throw new TaskRunnerError(`Command '${config.command}' requires Jira task scope.`);
773
+ }
774
+ }
775
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart") {
776
+ const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
639
777
  if (config.command === "auto") {
640
- await runAutoPipeline(config);
778
+ if (launchMode === "restart") {
779
+ resetAutoPipelineState(config);
780
+ }
781
+ await runAutoPipeline(config, setSummary, forceRefreshSummary);
641
782
  return false;
642
783
  }
643
784
  if (config.command === "auto-status") {
@@ -655,10 +796,18 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
655
796
  return false;
656
797
  }
657
798
  checkPrerequisites(config);
658
- process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
659
- process.env.JIRA_API_URL = config.jiraApiUrl;
660
- process.env.JIRA_TASK_FILE = config.jiraTaskFile;
799
+ if (config.scope.scopeType === "task") {
800
+ process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
801
+ process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
802
+ process.env.JIRA_TASK_FILE = config.jiraTaskFile ?? "";
803
+ }
804
+ else {
805
+ delete process.env.JIRA_BROWSE_URL;
806
+ delete process.env.JIRA_API_URL;
807
+ delete process.env.JIRA_TASK_FILE;
808
+ }
661
809
  if (config.command === "plan") {
810
+ requireTaskScopeConfig(config);
662
811
  if (config.verbose) {
663
812
  process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
664
813
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
@@ -668,10 +817,12 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
668
817
  jiraApiUrl: config.jiraApiUrl,
669
818
  taskKey: config.taskKey,
670
819
  extraPrompt: config.extraPrompt,
671
- }, requestUserInput);
820
+ forceRefresh: forceRefreshSummary,
821
+ }, requestUserInput, setSummary, launchMode);
672
822
  return false;
673
823
  }
674
824
  if (config.command === "bug-analyze") {
825
+ requireTaskScopeConfig(config);
675
826
  if (config.verbose) {
676
827
  process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
677
828
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
@@ -681,10 +832,30 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
681
832
  jiraApiUrl: config.jiraApiUrl,
682
833
  taskKey: config.taskKey,
683
834
  extraPrompt: config.extraPrompt,
684
- }, requestUserInput);
835
+ forceRefresh: forceRefreshSummary,
836
+ }, requestUserInput, setSummary, launchMode);
837
+ return false;
838
+ }
839
+ if (config.command === "gitlab-review") {
840
+ requireTaskScopeConfig(config);
841
+ requireJiraTaskFile(config.jiraTaskFile);
842
+ validateStructuredArtifacts([
843
+ { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
844
+ { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
845
+ ], "GitLab-review mode requires valid structured plan artifacts from the planning phase.");
846
+ const iteration = nextReviewIterationForTask(config.taskKey);
847
+ await runDeclarativeFlowBySpecFile("gitlab-review.json", config, {
848
+ taskKey: config.taskKey,
849
+ iteration,
850
+ extraPrompt: config.extraPrompt,
851
+ }, requestUserInput, undefined, launchMode);
852
+ if (!config.dryRun) {
853
+ printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}`);
854
+ }
685
855
  return false;
686
856
  }
687
857
  if (config.command === "bug-fix") {
858
+ requireTaskScopeConfig(config);
688
859
  requireJiraTaskFile(config.jiraTaskFile);
689
860
  requireArtifacts(bugAnalyzeArtifacts(config.taskKey), "Bug-fix mode requires bug-analyze artifacts from the bug analysis phase.");
690
861
  validateStructuredArtifacts([
@@ -695,69 +866,64 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
695
866
  await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
696
867
  taskKey: config.taskKey,
697
868
  extraPrompt: config.extraPrompt,
698
- }, requestUserInput);
869
+ }, requestUserInput, undefined, launchMode);
699
870
  return false;
700
871
  }
701
872
  if (config.command === "mr-description") {
873
+ requireTaskScopeConfig(config);
702
874
  requireJiraTaskFile(config.jiraTaskFile);
703
875
  await runDeclarativeFlowBySpecFile("mr-description.json", config, {
704
876
  taskKey: config.taskKey,
705
877
  extraPrompt: config.extraPrompt,
706
- }, requestUserInput);
878
+ }, requestUserInput, undefined, launchMode);
707
879
  return false;
708
880
  }
709
881
  if (config.command === "task-describe") {
710
- requireJiraTaskFile(config.jiraTaskFile);
882
+ requireTaskScopeConfig(config);
711
883
  await runDeclarativeFlowBySpecFile("task-describe.json", config, {
884
+ jiraApiUrl: config.jiraApiUrl,
712
885
  taskKey: config.taskKey,
713
886
  extraPrompt: config.extraPrompt,
714
- }, requestUserInput);
887
+ }, requestUserInput, undefined, launchMode);
715
888
  return false;
716
889
  }
717
890
  if (config.command === "implement") {
718
- requireJiraTaskFile(config.jiraTaskFile);
719
891
  requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
720
892
  validateStructuredArtifacts([
721
893
  { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
722
894
  { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
723
895
  { path: qaJsonFile(config.taskKey), schemaId: "qa-plan/v1" },
724
896
  ], "Implement mode requires valid structured plan artifacts from the planning phase.");
725
- try {
726
- await runDeclarativeFlowBySpecFile("implement.json", config, {
727
- taskKey: config.taskKey,
728
- dockerComposeFile: config.dockerComposeFile,
729
- extraPrompt: config.extraPrompt,
730
- runFollowupVerify,
731
- }, requestUserInput);
732
- }
733
- catch (error) {
734
- if (!config.dryRun) {
735
- const output = String(error.output ?? "");
736
- if (output.trim()) {
737
- printError("Build verification failed");
738
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
739
- }
740
- }
741
- throw error;
742
- }
897
+ await runDeclarativeFlowBySpecFile("implement.json", config, {
898
+ taskKey: config.taskKey,
899
+ extraPrompt: config.extraPrompt,
900
+ }, requestUserInput, undefined, launchMode);
743
901
  return false;
744
902
  }
745
903
  if (config.command === "review") {
746
- requireJiraTaskFile(config.jiraTaskFile);
747
- validateStructuredArtifacts([
748
- { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
749
- { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
750
- ], "Review mode requires valid structured plan artifacts from the planning phase.");
751
904
  const iteration = nextReviewIterationForTask(config.taskKey);
752
- await runDeclarativeFlowBySpecFile("review.json", config, {
753
- taskKey: config.taskKey,
754
- iteration,
755
- extraPrompt: config.extraPrompt,
756
- }, requestUserInput);
905
+ if (config.scope.scopeType === "task") {
906
+ requireTaskScopeConfig(config);
907
+ validateStructuredArtifacts([
908
+ { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
909
+ { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
910
+ ], "Review mode requires valid structured plan artifacts from the planning phase.");
911
+ await runDeclarativeFlowBySpecFile("review.json", config, {
912
+ taskKey: config.taskKey,
913
+ iteration,
914
+ extraPrompt: config.extraPrompt,
915
+ }, requestUserInput, undefined, launchMode);
916
+ }
917
+ else {
918
+ await runDeclarativeFlowBySpecFile("review-project.json", config, {
919
+ taskKey: config.taskKey,
920
+ iteration,
921
+ extraPrompt: config.extraPrompt,
922
+ }, requestUserInput, undefined, launchMode);
923
+ }
757
924
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
758
925
  }
759
926
  if (config.command === "review-fix") {
760
- requireJiraTaskFile(config.jiraTaskFile);
761
927
  const latestIteration = latestReviewReplyIteration(config.taskKey);
762
928
  if (latestIteration === null) {
763
929
  throw new TaskRunnerError("Review-fix mode requires at least one review-reply artifact.");
@@ -766,68 +932,27 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
766
932
  { path: reviewJsonFile(config.taskKey, latestIteration), schemaId: "review-findings/v1" },
767
933
  { path: reviewReplyJsonFile(config.taskKey, latestIteration), schemaId: "review-reply/v1" },
768
934
  ], "Review-fix mode requires valid structured review artifacts.");
769
- try {
770
- await runDeclarativeFlowBySpecFile("review-fix.json", config, {
771
- taskKey: config.taskKey,
772
- dockerComposeFile: config.dockerComposeFile,
773
- latestIteration,
774
- reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
775
- runFollowupVerify,
776
- extraPrompt: config.extraPrompt,
777
- reviewFixPoints: config.reviewFixPoints,
778
- }, requestUserInput);
779
- }
780
- catch (error) {
781
- if (!config.dryRun) {
782
- const output = String(error.output ?? "");
783
- if (output.trim()) {
784
- printError("Build verification failed");
785
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
786
- }
787
- }
788
- throw error;
789
- }
790
- return false;
791
- }
792
- if (config.command === "test") {
793
- requireJiraTaskFile(config.jiraTaskFile);
794
- try {
795
- await runDeclarativeFlowBySpecFile("test.json", config, {
796
- taskKey: config.taskKey,
797
- dockerComposeFile: config.dockerComposeFile,
798
- }, requestUserInput);
799
- }
800
- catch (error) {
801
- if (!config.dryRun) {
802
- const output = String(error.output ?? "");
803
- if (output.trim()) {
804
- printError("Build verification failed");
805
- printSummary("Build Failure Summary", await summarizeBuildFailure(output));
806
- }
807
- }
808
- throw error;
809
- }
810
- return false;
811
- }
812
- if (config.command === "test-fix" || config.command === "test-linter-fix") {
813
- requireJiraTaskFile(config.jiraTaskFile);
814
- await runDeclarativeFlowBySpecFile(config.command === "test-fix" ? "test-fix.json" : "test-linter-fix.json", config, {
935
+ await runDeclarativeFlowBySpecFile("review-fix.json", config, {
815
936
  taskKey: config.taskKey,
937
+ latestIteration,
938
+ reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
816
939
  extraPrompt: config.extraPrompt,
817
- }, requestUserInput);
940
+ reviewFixPoints: config.reviewFixPoints,
941
+ }, requestUserInput, undefined, launchMode);
818
942
  return false;
819
943
  }
820
- if (config.command === "run-tests-loop" || config.command === "run-linter-loop") {
821
- await runDeclarativeFlowBySpecFile(config.command === "run-tests-loop" ? "run-tests-loop.json" : "run-linter-loop.json", config, {
944
+ if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
945
+ await runDeclarativeFlowBySpecFile(config.command === "run-go-tests-loop" ? "run-go-tests-loop.json" : "run-go-linter-loop.json", config, {
822
946
  taskKey: config.taskKey,
823
- dockerComposeFile: config.dockerComposeFile,
947
+ runGoTestsScript: config.runGoTestsScript,
948
+ runGoLinterScript: config.runGoLinterScript,
824
949
  extraPrompt: config.extraPrompt,
825
- }, requestUserInput);
950
+ }, requestUserInput, undefined, launchMode);
826
951
  return false;
827
952
  }
828
953
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
829
954
  }
830
- async function runAutoPipelineDryRun(config) {
955
+ async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false) {
831
956
  checkAutoPrerequisites(config);
832
957
  printInfo("Dry-run auto pipeline from declarative spec");
833
958
  const autoFlow = loadAutoFlow();
@@ -840,15 +965,16 @@ async function runAutoPipelineDryRun(config) {
840
965
  publishFlowState("auto", executionState);
841
966
  for (const phase of autoFlow.phases) {
842
967
  printInfo(`Dry-run auto phase: ${phase.id}`);
843
- await runAutoPhaseViaSpec(config, phase.id, executionState);
968
+ await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary);
844
969
  if (executionState.terminated) {
845
970
  break;
846
971
  }
847
972
  }
848
973
  }
849
- async function runAutoPipeline(config) {
974
+ async function runAutoPipeline(config, setSummary, forceRefreshSummary = false) {
975
+ requireTaskScopeConfig(config);
850
976
  if (config.dryRun) {
851
- await runAutoPipelineDryRun(config);
977
+ await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary);
852
978
  return;
853
979
  }
854
980
  checkAutoPrerequisites(config);
@@ -888,7 +1014,7 @@ async function runAutoPipeline(config) {
888
1014
  saveAutoPipelineState(state);
889
1015
  try {
890
1016
  printInfo(`Running auto step: ${step.id}`);
891
- const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state);
1017
+ const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state, setSummary, forceRefreshSummary);
892
1018
  step.status = status;
893
1019
  step.finishedAt = nowIso8601();
894
1020
  step.returnCode = 0;
@@ -941,6 +1067,7 @@ function parseCliArgs(argv) {
941
1067
  let verbose = false;
942
1068
  let prompt;
943
1069
  let autoFromPhase;
1070
+ let scopeName;
944
1071
  let helpPhases = false;
945
1072
  let jiraRef;
946
1073
  for (let index = 1; index < argv.length; index += 1) {
@@ -962,6 +1089,11 @@ function parseCliArgs(argv) {
962
1089
  index += 1;
963
1090
  continue;
964
1091
  }
1092
+ if (token === "--scope") {
1093
+ scopeName = argv[index + 1];
1094
+ index += 1;
1095
+ continue;
1096
+ }
965
1097
  if (token === "--from") {
966
1098
  autoFromPhase = argv[index + 1];
967
1099
  index += 1;
@@ -973,41 +1105,92 @@ function parseCliArgs(argv) {
973
1105
  printAutoPhasesHelp();
974
1106
  process.exit(0);
975
1107
  }
976
- if (!jiraRef) {
977
- process.stderr.write(`${usage()}\n`);
978
- process.exit(1);
979
- }
980
1108
  return {
981
1109
  command: command,
982
- jiraRef,
983
1110
  dry,
984
1111
  verbose,
985
1112
  helpPhases,
1113
+ ...(jiraRef !== undefined ? { jiraRef } : {}),
1114
+ ...(scopeName !== undefined ? { scopeName } : {}),
986
1115
  ...(prompt !== undefined ? { prompt } : {}),
987
1116
  ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
988
1117
  };
989
1118
  }
990
1119
  function buildConfigFromArgs(args) {
991
- return buildConfig(args.command, args.jiraRef, {
1120
+ return buildBaseConfig(args.command, {
1121
+ ...(args.jiraRef !== undefined ? { jiraRef: args.jiraRef } : {}),
1122
+ ...(args.scopeName !== undefined ? { scopeName: args.scopeName } : {}),
992
1123
  ...(args.prompt !== undefined ? { extraPrompt: args.prompt } : {}),
993
1124
  ...(args.autoFromPhase !== undefined ? { autoFromPhase: args.autoFromPhase } : {}),
994
1125
  dryRun: args.dry,
995
1126
  verbose: args.verbose,
996
1127
  });
997
1128
  }
998
- async function runInteractive(jiraRef, forceRefresh = false) {
999
- const config = buildConfig("plan", jiraRef);
1000
- const jiraTaskPath = config.jiraTaskFile;
1129
+ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1130
+ let currentScope = jiraRef?.trim() ? resolveTaskScope(jiraRef, scopeName) : resolveProjectScope(scopeName);
1131
+ const currentIssueKey = currentScope.scopeKey;
1132
+ const gitBranchName = detectGitBranchName();
1001
1133
  let exiting = false;
1002
1134
  const ui = new InteractiveUi({
1003
- issueKey: config.jiraIssueKey,
1004
- summaryText: "Starting interactive session...",
1135
+ issueKey: currentIssueKey,
1136
+ summaryText: "",
1005
1137
  cwd: process.cwd(),
1138
+ gitBranchName,
1006
1139
  flows: interactiveFlowDefinitions(),
1007
- onRun: async (flowId) => {
1140
+ getRunConfirmation: async (flowId) => {
1141
+ if (flowId === "auto") {
1142
+ if (currentScope.scopeType !== "task") {
1143
+ return { resumeAvailable: false, hasExistingState: false };
1144
+ }
1145
+ const baseConfig = buildBaseConfig("auto", {
1146
+ jiraRef: currentScope.jiraRef,
1147
+ scopeName: currentScope.scopeKey !== currentScope.jiraIssueKey ? currentScope.scopeKey : null,
1148
+ });
1149
+ const state = loadAutoPipelineState(buildRuntimeConfig(baseConfig, currentScope));
1150
+ if (!state) {
1151
+ return { resumeAvailable: false, hasExistingState: false };
1152
+ }
1153
+ const status = deriveAutoPipelineStatus(state);
1154
+ if (status === "completed") {
1155
+ return { resumeAvailable: false, hasExistingState: true };
1156
+ }
1157
+ return {
1158
+ resumeAvailable: true,
1159
+ hasExistingState: true,
1160
+ details: buildAutoResumeDetails(state),
1161
+ };
1162
+ }
1163
+ if (commandRequiresTask(flowId) && currentScope.scopeType !== "task") {
1164
+ return { resumeAvailable: false, hasExistingState: false };
1165
+ }
1166
+ const state = loadFlowRunState(currentScope.scopeKey, flowId);
1167
+ if (!state || !hasResumableFlowState(state)) {
1168
+ return { resumeAvailable: false, hasExistingState: Boolean(state) };
1169
+ }
1170
+ return {
1171
+ resumeAvailable: true,
1172
+ hasExistingState: true,
1173
+ details: buildFlowResumeDetails(state),
1174
+ };
1175
+ },
1176
+ onRun: async (flowId, launchMode) => {
1008
1177
  try {
1009
- const command = buildConfig(flowId, jiraRef);
1010
- await executeCommand(command, true, (form) => ui.requestUserInput(form));
1178
+ const previousScopeType = currentScope.scopeType;
1179
+ const previousScopeKey = currentScope.scopeKey;
1180
+ const baseConfig = buildBaseConfig(flowId, {
1181
+ ...(currentScope.scopeType === "task" ? { jiraRef: currentScope.jiraRef } : {}),
1182
+ ...(currentScope.scopeType === "task" && currentScope.scopeKey !== currentScope.jiraIssueKey
1183
+ ? { scopeName: currentScope.scopeKey }
1184
+ : {}),
1185
+ ...(currentScope.scopeType === "project" ? { scopeName: currentScope.scopeKey } : {}),
1186
+ });
1187
+ const nextScope = await resolveScopeForCommand(baseConfig, (form) => ui.requestUserInput(form));
1188
+ currentScope = nextScope;
1189
+ ui.setIssueKey(currentScope.scopeKey);
1190
+ if (currentScope.scopeType === "task" && (previousScopeType !== "task" || previousScopeKey !== currentScope.scopeKey)) {
1191
+ syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1192
+ }
1193
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode);
1011
1194
  }
1012
1195
  catch (error) {
1013
1196
  if (error instanceof TaskRunnerError) {
@@ -1029,52 +1212,13 @@ async function runInteractive(jiraRef, forceRefresh = false) {
1029
1212
  },
1030
1213
  });
1031
1214
  ui.mount();
1032
- printInfo(`Interactive mode for ${config.jiraIssueKey}`);
1215
+ printInfo(`Interactive mode for ${currentScope.scopeKey}`);
1033
1216
  printInfo("Use h to see help.");
1034
- try {
1035
- ui.setBusy(true, "preflight");
1036
- const preflightState = await runPreflightFlow(createPipelineContext({
1037
- issueKey: config.taskKey,
1038
- jiraRef: config.jiraRef,
1039
- dryRun: false,
1040
- verbose: config.verbose,
1041
- runtime: runtimeServices,
1042
- setSummary: (markdown) => {
1043
- ui.setSummary(markdown);
1044
- },
1045
- requestUserInput: (form) => ui.requestUserInput(form),
1046
- }), {
1047
- jiraApiUrl: config.jiraApiUrl,
1048
- jiraTaskFile: config.jiraTaskFile,
1049
- taskKey: config.taskKey,
1050
- forceRefresh,
1051
- });
1052
- const preflightPhase = preflightState.phases.find((phase) => phase.id === "preflight");
1053
- if (preflightPhase) {
1054
- ui.appendLog("[preflight] completed");
1055
- for (const step of preflightPhase.steps) {
1056
- ui.appendLog(`[preflight] ${step.id}: ${step.status}`);
1057
- }
1058
- }
1059
- if (!existsSync(jiraTaskPath)) {
1060
- throw new TaskRunnerError(`Preflight finished without Jira task file: ${jiraTaskPath}\n` +
1061
- "Jira fetch did not complete successfully. Check JIRA_API_KEY and Jira connectivity.");
1062
- }
1063
- if (!existsSync(taskSummaryFile(config.taskKey))) {
1064
- ui.appendLog("[preflight] task summary file was not created");
1065
- ui.setSummary("Task summary is not available yet. Select and run `plan` or refresh Jira data.");
1066
- }
1217
+ if (currentScope.scopeType !== "task") {
1218
+ ui.appendLog("[scope] project scope active; task summary will appear after a Jira-backed flow runs");
1067
1219
  }
1068
- catch (error) {
1069
- if (error instanceof TaskRunnerError) {
1070
- printError(error.message);
1071
- }
1072
- else {
1073
- throw error;
1074
- }
1075
- }
1076
- finally {
1077
- ui.setBusy(false);
1220
+ if (currentScope.scopeType === "task") {
1221
+ syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1078
1222
  }
1079
1223
  return await new Promise((resolve, reject) => {
1080
1224
  const interval = setInterval(() => {
@@ -1102,6 +1246,9 @@ export async function main(argv = process.argv.slice(2)) {
1102
1246
  args.shift();
1103
1247
  }
1104
1248
  try {
1249
+ if (args.length === 0) {
1250
+ return await runInteractive(undefined, forceRefresh);
1251
+ }
1105
1252
  if (args.length === 1 && !args[0]?.startsWith("-") && !COMMANDS.includes(args[0])) {
1106
1253
  return await runInteractive(args[0] ?? "", forceRefresh);
1107
1254
  }