agentweaver 0.1.8 → 0.1.10

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 (55) hide show
  1. package/README.md +71 -25
  2. package/dist/artifacts.js +25 -1
  3. package/dist/errors.js +7 -0
  4. package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
  5. package/dist/executors/configs/opencode-config.js +6 -0
  6. package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
  7. package/dist/executors/jira-fetch-executor.js +8 -2
  8. package/dist/executors/opencode-executor.js +35 -0
  9. package/dist/gitlab.js +199 -5
  10. package/dist/index.js +215 -144
  11. package/dist/interactive-ui.js +363 -37
  12. package/dist/jira.js +116 -14
  13. package/dist/pipeline/auto-flow.js +1 -1
  14. package/dist/pipeline/declarative-flows.js +44 -6
  15. package/dist/pipeline/flow-catalog.js +73 -0
  16. package/dist/pipeline/flow-specs/auto.json +183 -1
  17. package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
  18. package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
  19. package/dist/pipeline/flow-specs/opencode/auto-opencode.json +1365 -0
  20. package/dist/pipeline/flow-specs/opencode/bugz/bug-analyze-opencode.json +382 -0
  21. package/dist/pipeline/flow-specs/opencode/bugz/bug-fix-opencode.json +56 -0
  22. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-diff-review-opencode.json +308 -0
  23. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-review-opencode.json +437 -0
  24. package/dist/pipeline/flow-specs/opencode/gitlab/mr-description-opencode.json +117 -0
  25. package/dist/pipeline/flow-specs/opencode/go/run-go-linter-loop-opencode.json +321 -0
  26. package/dist/pipeline/flow-specs/opencode/go/run-go-tests-loop-opencode.json +321 -0
  27. package/dist/pipeline/flow-specs/opencode/implement-opencode.json +64 -0
  28. package/dist/pipeline/flow-specs/opencode/plan-opencode.json +603 -0
  29. package/dist/pipeline/flow-specs/opencode/review/review-fix-opencode.json +209 -0
  30. package/dist/pipeline/flow-specs/opencode/review/review-opencode.json +452 -0
  31. package/dist/pipeline/flow-specs/opencode/task-describe-opencode.json +148 -0
  32. package/dist/pipeline/flow-specs/plan.json +183 -1
  33. package/dist/pipeline/node-registry.js +80 -8
  34. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
  35. package/dist/pipeline/nodes/flow-run-node.js +2 -2
  36. package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
  37. package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
  38. package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
  39. package/dist/pipeline/nodes/user-input-node.js +9 -1
  40. package/dist/pipeline/prompt-registry.js +3 -1
  41. package/dist/pipeline/registry.js +10 -0
  42. package/dist/pipeline/spec-loader.js +48 -3
  43. package/dist/pipeline/spec-types.js +43 -1
  44. package/dist/pipeline/spec-validator.js +53 -7
  45. package/dist/pipeline/value-resolver.js +15 -1
  46. package/dist/prompts.js +47 -12
  47. package/dist/runtime/process-runner.js +45 -1
  48. package/dist/scope.js +24 -32
  49. package/dist/structured-artifact-schemas.json +154 -1
  50. package/dist/structured-artifacts.js +2 -0
  51. package/dist/user-input.js +7 -0
  52. package/package.json +1 -1
  53. package/dist/pipeline/flow-specs/preflight.json +0 -206
  54. package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
  55. package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ 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, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
7
- import { TaskRunnerError } from "./errors.js";
6
+ import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewFile, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
7
+ import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
8
8
  import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
9
9
  import { requireJiraTaskFile } from "./jira.js";
10
10
  import { validateStructuredArtifacts } from "./structured-artifacts.js";
@@ -13,16 +13,18 @@ import { createPipelineContext } from "./pipeline/context.js";
13
13
  import { loadAutoFlow } from "./pipeline/auto-flow.js";
14
14
  import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
15
15
  import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
16
+ import { findCatalogEntry, isBuiltInCommandFlowId, loadInteractiveFlowCatalog, toDeclarativeFlowRef } from "./pipeline/flow-catalog.js";
16
17
  import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
17
18
  import { agentweaverHome, defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
18
19
  import { runCommand } from "./runtime/process-runner.js";
19
20
  import { InteractiveUi } from "./interactive-ui.js";
20
21
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
21
22
  import { requestUserInputInTerminal } from "./user-input.js";
22
- import { detectGitBranchName, requestTaskScope, resolveProjectScope, resolveTaskScope, } from "./scope.js";
23
+ import { attachJiraContext, detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
23
24
  const COMMANDS = [
24
25
  "bug-analyze",
25
26
  "bug-fix",
27
+ "gitlab-diff-review",
26
28
  "gitlab-review",
27
29
  "mr-description",
28
30
  "plan",
@@ -39,12 +41,15 @@ const COMMANDS = [
39
41
  const AUTO_STATE_SCHEMA_VERSION = 3;
40
42
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
41
43
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
42
- const runtimeServices = {
43
- resolveCmd,
44
- resolveDockerComposeCmd,
45
- dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
46
- runCommand,
47
- };
44
+ function createRuntimeServices(signal) {
45
+ return {
46
+ resolveCmd,
47
+ resolveDockerComposeCmd,
48
+ dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
49
+ runCommand: (argv, options = {}) => runCommand(argv, { ...options, ...(signal ? { signal } : {}) }),
50
+ };
51
+ }
52
+ const runtimeServices = createRuntimeServices();
48
53
  function buildFailureOutputPreview(output) {
49
54
  const normalized = stripAnsi(output).replace(/\r\n/g, "\n").trim();
50
55
  if (!normalized) {
@@ -81,7 +86,8 @@ function usage() {
81
86
  agentweaver
82
87
  agentweaver <jira-browse-url|jira-issue-key>
83
88
  agentweaver --force <jira-browse-url|jira-issue-key>
84
- agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>] <jira-browse-url|jira-issue-key>
89
+ agentweaver gitlab-diff-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
90
+ agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
85
91
  agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
86
92
  agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
87
93
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
@@ -100,8 +106,8 @@ function usage() {
100
106
 
101
107
  Interactive Mode:
102
108
  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.
104
- Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
109
+ If a Jira task is provided, interactive mode starts in the current project scope with Jira context attached.
110
+ Use Up/Down to move in the flow tree, Left/Right to collapse or expand folders, Enter to toggle a folder or run a flow, h for help, q to exit.
105
111
 
106
112
  Flags:
107
113
  --version Show package version
@@ -121,12 +127,15 @@ Optional environment variables:
121
127
  DOCKER_COMPOSE_BIN
122
128
  CODEX_BIN
123
129
  CODEX_MODEL
130
+ OPENCODE_BIN
131
+ OPENCODE_MODEL
124
132
  CLAUDE_BIN
125
133
  CLAUDE_MODEL
126
134
 
127
135
  Notes:
128
136
  - 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.`;
137
+ - All flow state and artifacts are stored in the current project scope by default.
138
+ - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.`;
130
139
  }
131
140
  function packageVersion() {
132
141
  const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
@@ -334,6 +343,20 @@ function buildFlowResumeDetails(state) {
334
343
  }
335
344
  return lines.join("\n");
336
345
  }
346
+ function lookupInteractiveFlowResume(flowEntry, currentScope) {
347
+ const directState = loadFlowRunState(currentScope.scopeKey, flowEntry.id);
348
+ if (directState && hasResumableFlowState(directState)) {
349
+ return {
350
+ resumeAvailable: true,
351
+ hasExistingState: true,
352
+ details: buildFlowResumeDetails(directState),
353
+ };
354
+ }
355
+ return {
356
+ resumeAvailable: false,
357
+ hasExistingState: Boolean(directState),
358
+ };
359
+ }
337
360
  function printAutoPhasesHelp() {
338
361
  const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
339
362
  phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
@@ -423,7 +446,6 @@ function commandRequiresTask(command) {
423
446
  return (command === "plan" ||
424
447
  command === "bug-analyze" ||
425
448
  command === "bug-fix" ||
426
- command === "gitlab-review" ||
427
449
  command === "mr-description" ||
428
450
  command === "task-describe" ||
429
451
  command === "auto" ||
@@ -431,7 +453,9 @@ function commandRequiresTask(command) {
431
453
  command === "auto-reset");
432
454
  }
433
455
  function commandSupportsProjectScope(command) {
434
- return (command === "implement" ||
456
+ return (command === "gitlab-diff-review" ||
457
+ command === "gitlab-review" ||
458
+ command === "implement" ||
435
459
  command === "review" ||
436
460
  command === "review-fix" ||
437
461
  command === "run-go-tests-loop" ||
@@ -439,12 +463,12 @@ function commandSupportsProjectScope(command) {
439
463
  }
440
464
  async function resolveScopeForCommand(config, requestUserInput) {
441
465
  if (config.jiraRef?.trim()) {
442
- return resolveTaskScope(config.jiraRef, config.scopeName);
466
+ return resolveProjectScope(config.scopeName, config.jiraRef);
443
467
  }
444
468
  if (commandRequiresTask(config.command)) {
445
469
  try {
446
- const taskScope = await requestTaskScope(requestUserInput);
447
- return config.scopeName ? resolveTaskScope(taskScope.jiraRef, config.scopeName) : taskScope;
470
+ const jiraContext = await requestJiraContext(requestUserInput);
471
+ return resolveProjectScope(config.scopeName, jiraContext.jiraRef);
448
472
  }
449
473
  catch (error) {
450
474
  if (error instanceof TaskRunnerError && error.message.includes("no TTY is available")) {
@@ -461,22 +485,14 @@ async function resolveScopeForCommand(config, requestUserInput) {
461
485
  }
462
486
  function buildRuntimeConfig(baseConfig, scope) {
463
487
  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
488
  return {
476
489
  ...baseConfig,
477
490
  scope,
478
491
  taskKey: scope.scopeKey,
479
- jiraRef: scope.scopeKey,
492
+ jiraRef: scope.jiraRef ?? scope.scopeKey,
493
+ ...(scope.jiraBrowseUrl ? { jiraBrowseUrl: scope.jiraBrowseUrl } : {}),
494
+ ...(scope.jiraApiUrl ? { jiraApiUrl: scope.jiraApiUrl } : {}),
495
+ ...(scope.jiraTaskFile ? { jiraTaskFile: scope.jiraTaskFile } : {}),
480
496
  };
481
497
  }
482
498
  function checkPrerequisites(config) {
@@ -490,7 +506,7 @@ function checkPrerequisites(config) {
490
506
  config.command === "run-go-linter-loop") {
491
507
  resolveCmd("codex", "CODEX_BIN");
492
508
  }
493
- if (config.command === "review") {
509
+ if (config.command === "review" || config.command === "gitlab-diff-review") {
494
510
  resolveCmd("claude", "CLAUDE_BIN");
495
511
  }
496
512
  }
@@ -514,6 +530,7 @@ function autoFlowParams(config, forceRefreshSummary = false) {
514
530
  const FLOW_DESCRIPTIONS = {
515
531
  auto: "Полный пайплайн задачи: планирование, реализация, проверки, ревью, ответы на ревью и повторные итерации до готовности к merge.",
516
532
  "bug-analyze": "Анализирует баг по Jira и создаёт структурированные артефакты: гипотезу причины, дизайн исправления и план работ.",
533
+ "gitlab-diff-review": "Запрашивает GitLab MR URL через user-input, загружает diff merge request по API и запускает код-ревью через Claude Opus с сохранением markdown и structured JSON artifacts.",
517
534
  "gitlab-review": "Запрашивает GitLab MR URL через user-input, загружает комментарии код-ревью по API и сохраняет markdown плюс structured JSON artifact.",
518
535
  "bug-fix": "Берёт результаты bug-analyze как source of truth и реализует исправление бага в коде.",
519
536
  "mr-description": "Готовит краткое intent-описание для merge request на основе задачи и текущих изменений.",
@@ -528,12 +545,15 @@ const FLOW_DESCRIPTIONS = {
528
545
  function flowDescription(id) {
529
546
  return FLOW_DESCRIPTIONS[id] ?? "Описание для этого flow пока не задано.";
530
547
  }
531
- function declarativeFlowDefinition(id, label, fileName) {
532
- const flow = loadDeclarativeFlow(fileName);
548
+ function interactiveFlowDefinition(entry) {
549
+ const flow = entry.flow;
533
550
  return {
534
- id,
535
- label,
536
- description: flowDescription(id),
551
+ id: entry.id,
552
+ label: entry.id,
553
+ description: flowDescription(entry.id),
554
+ source: entry.source,
555
+ treePath: [...entry.treePath],
556
+ ...(entry.source === "project-local" ? { sourcePath: entry.absolutePath } : {}),
537
557
  phases: flow.phases.map((phase) => ({
538
558
  id: phase.id,
539
559
  repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
@@ -543,36 +563,8 @@ function declarativeFlowDefinition(id, label, fileName) {
543
563
  })),
544
564
  };
545
565
  }
546
- function autoFlowDefinition() {
547
- const flow = loadAutoFlow();
548
- return {
549
- id: "auto",
550
- label: "auto",
551
- description: flowDescription("auto"),
552
- phases: flow.phases.map((phase) => ({
553
- id: phase.id,
554
- repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
555
- steps: phase.steps.map((step) => ({
556
- id: step.id,
557
- })),
558
- })),
559
- };
560
- }
561
- function interactiveFlowDefinitions() {
562
- return [
563
- autoFlowDefinition(),
564
- declarativeFlowDefinition("bug-analyze", "bug-analyze", "bug-analyze.json"),
565
- declarativeFlowDefinition("bug-fix", "bug-fix", "bug-fix.json"),
566
- declarativeFlowDefinition("gitlab-review", "gitlab-review", "gitlab-review.json"),
567
- declarativeFlowDefinition("mr-description", "mr-description", "mr-description.json"),
568
- declarativeFlowDefinition("plan", "plan", "plan.json"),
569
- declarativeFlowDefinition("task-describe", "task-describe", "task-describe.json"),
570
- declarativeFlowDefinition("implement", "implement", "implement.json"),
571
- declarativeFlowDefinition("review", "review", "review.json"),
572
- declarativeFlowDefinition("review-fix", "review-fix", "review-fix.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"),
575
- ];
566
+ function interactiveFlowDefinitions(catalog) {
567
+ return catalog.map((entry) => interactiveFlowDefinition(entry));
576
568
  }
577
569
  function publishFlowState(flowId, executionState) {
578
570
  setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
@@ -586,7 +578,7 @@ function loadTaskSummaryMarkdown(taskKey) {
586
578
  return markdown.length > 0 ? markdown : null;
587
579
  }
588
580
  function syncInteractiveTaskSummary(ui, scope, forceRefresh = false) {
589
- if (scope.scopeType !== "task" || forceRefresh) {
581
+ if (forceRefresh) {
590
582
  ui.clearSummary();
591
583
  return;
592
584
  }
@@ -610,38 +602,38 @@ function findCurrentFlowExecutionStep(state) {
610
602
  }
611
603
  return null;
612
604
  }
613
- async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
605
+ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
614
606
  const context = createPipelineContext({
615
607
  issueKey: config.taskKey,
616
608
  jiraRef: config.jiraRef,
617
609
  dryRun: config.dryRun,
618
610
  verbose: config.verbose,
619
- runtime: runtimeServices,
611
+ runtime,
620
612
  ...(setSummary ? { setSummary } : {}),
621
613
  requestUserInput,
622
614
  });
623
- const flow = loadDeclarativeFlow(fileName);
615
+ const flow = loadDeclarativeFlow(flowRef);
624
616
  const initialExecutionState = {
625
617
  flowKind: flow.kind,
626
618
  flowVersion: flow.version,
627
619
  terminated: false,
628
620
  phases: [],
629
621
  };
630
- let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, config.command) : null;
622
+ let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
631
623
  if (persistedState && launchMode === "resume") {
632
624
  persistedState = prepareFlowStateForResume(persistedState);
633
625
  }
634
626
  else if (launchMode === "restart") {
635
- resetFlowRunState(config.scope.scopeKey, config.command);
627
+ resetFlowRunState(config.scope.scopeKey, flowId);
636
628
  }
637
629
  const executionState = persistedState?.executionState ?? initialExecutionState;
638
- const state = persistedState ?? createFlowRunState(config.scope.scopeKey, config.command, executionState);
630
+ const state = persistedState ?? createFlowRunState(config.scope.scopeKey, flowId, executionState);
639
631
  state.status = "running";
640
632
  state.lastError = null;
641
633
  state.currentStep = findCurrentFlowExecutionStep(state);
642
634
  state.executionState = executionState;
643
635
  saveFlowRunState(state);
644
- publishFlowState(config.command, executionState);
636
+ publishFlowState(flowId, executionState);
645
637
  try {
646
638
  for (const phase of flow.phases) {
647
639
  await runExpandedPhase(phase, context, flowParams, flow.constants, {
@@ -652,7 +644,7 @@ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, reques
652
644
  state.executionState = nextExecutionState;
653
645
  state.currentStep = findCurrentFlowExecutionStep(state);
654
646
  saveFlowRunState(state);
655
- publishFlowState(config.command, nextExecutionState);
647
+ publishFlowState(flowId, nextExecutionState);
656
648
  },
657
649
  onStepStart: async (currentPhase, step) => {
658
650
  state.currentStep = `${currentPhase.id}:${step.id}`;
@@ -681,13 +673,59 @@ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, reques
681
673
  throw error;
682
674
  }
683
675
  }
684
- async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false) {
676
+ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
677
+ await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, flowParams, requestUserInput, setSummary, launchMode, runtime);
678
+ }
679
+ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false) {
680
+ const iteration = nextReviewIterationForTask(config.taskKey);
681
+ const latestIteration = latestReviewReplyIteration(config.taskKey);
682
+ return {
683
+ taskKey: config.taskKey,
684
+ jiraRef: config.jiraRef,
685
+ jiraBrowseUrl: config.jiraBrowseUrl,
686
+ jiraApiUrl: config.jiraApiUrl,
687
+ jiraTaskFile: config.jiraTaskFile,
688
+ scopeKey: config.scope.scopeKey,
689
+ dockerComposeFile: config.dockerComposeFile,
690
+ runGoTestsScript: config.runGoTestsScript,
691
+ runGoLinterScript: config.runGoLinterScript,
692
+ runGoCoverageScript: config.runGoCoverageScript,
693
+ extraPrompt: config.extraPrompt,
694
+ reviewFixPoints: config.reviewFixPoints,
695
+ iteration,
696
+ latestIteration,
697
+ ...(latestIteration !== null ? { reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration) } : {}),
698
+ forceRefresh: forceRefreshSummary,
699
+ };
700
+ }
701
+ const TASK_SCOPE_PARAM_REFS = new Set(["params.jiraApiUrl", "params.jiraBrowseUrl", "params.jiraTaskFile"]);
702
+ function valueReferencesTaskScopeParams(value) {
703
+ if (Array.isArray(value)) {
704
+ return value.some((item) => valueReferencesTaskScopeParams(item));
705
+ }
706
+ if (!value || typeof value !== "object") {
707
+ return false;
708
+ }
709
+ if ("ref" in value &&
710
+ typeof value.ref === "string" &&
711
+ TASK_SCOPE_PARAM_REFS.has(value.ref)) {
712
+ return true;
713
+ }
714
+ return Object.values(value).some((item) => valueReferencesTaskScopeParams(item));
715
+ }
716
+ function flowRequiresTaskScope(entry) {
717
+ if (entry.source === "built-in" && isBuiltInCommandFlowId(entry.id)) {
718
+ return commandRequiresTask(entry.id);
719
+ }
720
+ return valueReferencesTaskScopeParams(entry.flow.phases);
721
+ }
722
+ async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
685
723
  const context = createPipelineContext({
686
724
  issueKey: config.taskKey,
687
725
  jiraRef: config.jiraRef,
688
726
  dryRun: config.dryRun,
689
727
  verbose: config.verbose,
690
- runtime: runtimeServices,
728
+ runtime,
691
729
  ...(setSummary ? { setSummary } : {}),
692
730
  requestUserInput: requestUserInputInTerminal,
693
731
  });
@@ -767,18 +805,18 @@ async function summarizeBuildFailure(output) {
767
805
  requestUserInput: requestUserInputInTerminal,
768
806
  }), output);
769
807
  }
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.`);
808
+ function requireJiraConfig(config) {
809
+ if (!config.jiraBrowseUrl || !config.jiraApiUrl || !config.jiraTaskFile) {
810
+ throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
773
811
  }
774
812
  }
775
- async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart") {
813
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", runtime = runtimeServices) {
776
814
  const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
777
815
  if (config.command === "auto") {
778
816
  if (launchMode === "restart") {
779
817
  resetAutoPipelineState(config);
780
818
  }
781
- await runAutoPipeline(config, setSummary, forceRefreshSummary);
819
+ await runAutoPipeline(config, setSummary, forceRefreshSummary, runtime);
782
820
  return false;
783
821
  }
784
822
  if (config.command === "auto-status") {
@@ -796,7 +834,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
796
834
  return false;
797
835
  }
798
836
  checkPrerequisites(config);
799
- if (config.scope.scopeType === "task") {
837
+ if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
800
838
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
801
839
  process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
802
840
  process.env.JIRA_TASK_FILE = config.jiraTaskFile ?? "";
@@ -807,7 +845,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
807
845
  delete process.env.JIRA_TASK_FILE;
808
846
  }
809
847
  if (config.command === "plan") {
810
- requireTaskScopeConfig(config);
848
+ requireJiraConfig(config);
811
849
  if (config.verbose) {
812
850
  process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
813
851
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
@@ -818,11 +856,11 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
818
856
  taskKey: config.taskKey,
819
857
  extraPrompt: config.extraPrompt,
820
858
  forceRefresh: forceRefreshSummary,
821
- }, requestUserInput, setSummary, launchMode);
859
+ }, requestUserInput, setSummary, launchMode, runtime);
822
860
  return false;
823
861
  }
824
862
  if (config.command === "bug-analyze") {
825
- requireTaskScopeConfig(config);
863
+ requireJiraConfig(config);
826
864
  if (config.verbose) {
827
865
  process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
828
866
  process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
@@ -833,29 +871,35 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
833
871
  taskKey: config.taskKey,
834
872
  extraPrompt: config.extraPrompt,
835
873
  forceRefresh: forceRefreshSummary,
836
- }, requestUserInput, setSummary, launchMode);
874
+ }, requestUserInput, setSummary, launchMode, runtime);
837
875
  return false;
838
876
  }
839
877
  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
878
  const iteration = nextReviewIterationForTask(config.taskKey);
847
879
  await runDeclarativeFlowBySpecFile("gitlab-review.json", config, {
848
880
  taskKey: config.taskKey,
849
881
  iteration,
850
882
  extraPrompt: config.extraPrompt,
851
- }, requestUserInput, undefined, launchMode);
883
+ }, requestUserInput, undefined, launchMode, runtime);
852
884
  if (!config.dryRun) {
853
885
  printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}`);
854
886
  }
855
887
  return false;
856
888
  }
889
+ if (config.command === "gitlab-diff-review") {
890
+ const iteration = nextReviewIterationForTask(config.taskKey);
891
+ await runDeclarativeFlowBySpecFile("gitlab-diff-review.json", config, {
892
+ taskKey: config.taskKey,
893
+ iteration,
894
+ extraPrompt: config.extraPrompt,
895
+ }, requestUserInput, undefined, launchMode, runtime);
896
+ if (!config.dryRun) {
897
+ printSummary("GitLab Diff Review", `Artifacts:\n${gitlabDiffFile(config.taskKey)}\n${gitlabDiffJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}`);
898
+ }
899
+ return false;
900
+ }
857
901
  if (config.command === "bug-fix") {
858
- requireTaskScopeConfig(config);
902
+ requireJiraConfig(config);
859
903
  requireJiraTaskFile(config.jiraTaskFile);
860
904
  requireArtifacts(bugAnalyzeArtifacts(config.taskKey), "Bug-fix mode requires bug-analyze artifacts from the bug analysis phase.");
861
905
  validateStructuredArtifacts([
@@ -866,25 +910,25 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
866
910
  await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
867
911
  taskKey: config.taskKey,
868
912
  extraPrompt: config.extraPrompt,
869
- }, requestUserInput, undefined, launchMode);
913
+ }, requestUserInput, undefined, launchMode, runtime);
870
914
  return false;
871
915
  }
872
916
  if (config.command === "mr-description") {
873
- requireTaskScopeConfig(config);
917
+ requireJiraConfig(config);
874
918
  requireJiraTaskFile(config.jiraTaskFile);
875
919
  await runDeclarativeFlowBySpecFile("mr-description.json", config, {
876
920
  taskKey: config.taskKey,
877
921
  extraPrompt: config.extraPrompt,
878
- }, requestUserInput, undefined, launchMode);
922
+ }, requestUserInput, undefined, launchMode, runtime);
879
923
  return false;
880
924
  }
881
925
  if (config.command === "task-describe") {
882
- requireTaskScopeConfig(config);
926
+ requireJiraConfig(config);
883
927
  await runDeclarativeFlowBySpecFile("task-describe.json", config, {
884
928
  jiraApiUrl: config.jiraApiUrl,
885
929
  taskKey: config.taskKey,
886
930
  extraPrompt: config.extraPrompt,
887
- }, requestUserInput, undefined, launchMode);
931
+ }, requestUserInput, undefined, launchMode, runtime);
888
932
  return false;
889
933
  }
890
934
  if (config.command === "implement") {
@@ -902,8 +946,8 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
902
946
  }
903
947
  if (config.command === "review") {
904
948
  const iteration = nextReviewIterationForTask(config.taskKey);
905
- if (config.scope.scopeType === "task") {
906
- requireTaskScopeConfig(config);
949
+ if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
950
+ requireJiraConfig(config);
907
951
  validateStructuredArtifacts([
908
952
  { path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
909
953
  { path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
@@ -912,14 +956,14 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
912
956
  taskKey: config.taskKey,
913
957
  iteration,
914
958
  extraPrompt: config.extraPrompt,
915
- }, requestUserInput, undefined, launchMode);
959
+ }, requestUserInput, undefined, launchMode, runtime);
916
960
  }
917
961
  else {
918
962
  await runDeclarativeFlowBySpecFile("review-project.json", config, {
919
963
  taskKey: config.taskKey,
920
964
  iteration,
921
965
  extraPrompt: config.extraPrompt,
922
- }, requestUserInput, undefined, launchMode);
966
+ }, requestUserInput, undefined, launchMode, runtime);
923
967
  }
924
968
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
925
969
  }
@@ -938,7 +982,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
938
982
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
939
983
  extraPrompt: config.extraPrompt,
940
984
  reviewFixPoints: config.reviewFixPoints,
941
- }, requestUserInput, undefined, launchMode);
985
+ }, requestUserInput, undefined, launchMode, runtime);
942
986
  return false;
943
987
  }
944
988
  if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
@@ -947,12 +991,12 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
947
991
  runGoTestsScript: config.runGoTestsScript,
948
992
  runGoLinterScript: config.runGoLinterScript,
949
993
  extraPrompt: config.extraPrompt,
950
- }, requestUserInput, undefined, launchMode);
994
+ }, requestUserInput, undefined, launchMode, runtime);
951
995
  return false;
952
996
  }
953
997
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
954
998
  }
955
- async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false) {
999
+ async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
956
1000
  checkAutoPrerequisites(config);
957
1001
  printInfo("Dry-run auto pipeline from declarative spec");
958
1002
  const autoFlow = loadAutoFlow();
@@ -965,16 +1009,16 @@ async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = f
965
1009
  publishFlowState("auto", executionState);
966
1010
  for (const phase of autoFlow.phases) {
967
1011
  printInfo(`Dry-run auto phase: ${phase.id}`);
968
- await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary);
1012
+ await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary, runtime);
969
1013
  if (executionState.terminated) {
970
1014
  break;
971
1015
  }
972
1016
  }
973
1017
  }
974
- async function runAutoPipeline(config, setSummary, forceRefreshSummary = false) {
975
- requireTaskScopeConfig(config);
1018
+ async function runAutoPipeline(config, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
1019
+ requireJiraConfig(config);
976
1020
  if (config.dryRun) {
977
- await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary);
1021
+ await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary, runtime);
978
1022
  return;
979
1023
  }
980
1024
  checkAutoPrerequisites(config);
@@ -1014,7 +1058,7 @@ async function runAutoPipeline(config, setSummary, forceRefreshSummary = false)
1014
1058
  saveAutoPipelineState(state);
1015
1059
  try {
1016
1060
  printInfo(`Running auto step: ${step.id}`);
1017
- const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state, setSummary, forceRefreshSummary);
1061
+ const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state, setSummary, forceRefreshSummary, runtime);
1018
1062
  step.status = status;
1019
1063
  step.finishedAt = nowIso8601();
1020
1064
  step.returnCode = 0;
@@ -1127,24 +1171,31 @@ function buildConfigFromArgs(args) {
1127
1171
  });
1128
1172
  }
1129
1173
  async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1130
- let currentScope = jiraRef?.trim() ? resolveTaskScope(jiraRef, scopeName) : resolveProjectScope(scopeName);
1174
+ let currentScope = resolveProjectScope(scopeName, jiraRef);
1131
1175
  const gitBranchName = detectGitBranchName();
1176
+ const flowCatalog = loadInteractiveFlowCatalog(process.cwd());
1177
+ let activeAbortController = null;
1178
+ let activeFlowId = null;
1132
1179
  let exiting = false;
1133
1180
  const ui = new InteractiveUi({
1134
1181
  scopeKey: currentScope.scopeKey,
1135
- jiraIssueKey: currentScope.scopeType === "task" ? currentScope.jiraIssueKey : null,
1182
+ jiraIssueKey: currentScope.jiraIssueKey ?? null,
1136
1183
  summaryText: "",
1137
1184
  cwd: process.cwd(),
1138
1185
  gitBranchName,
1139
- flows: interactiveFlowDefinitions(),
1186
+ flows: interactiveFlowDefinitions(flowCatalog),
1140
1187
  getRunConfirmation: async (flowId) => {
1188
+ const flowEntry = findCatalogEntry(flowId, flowCatalog);
1189
+ if (!flowEntry) {
1190
+ throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1191
+ }
1141
1192
  if (flowId === "auto") {
1142
- if (currentScope.scopeType !== "task") {
1193
+ if (!currentScope.jiraRef) {
1143
1194
  return { resumeAvailable: false, hasExistingState: false };
1144
1195
  }
1145
1196
  const baseConfig = buildBaseConfig("auto", {
1146
1197
  jiraRef: currentScope.jiraRef,
1147
- scopeName: currentScope.scopeKey !== currentScope.jiraIssueKey ? currentScope.scopeKey : null,
1198
+ scopeName: currentScope.scopeKey,
1148
1199
  });
1149
1200
  const state = loadAutoPipelineState(buildRuntimeConfig(baseConfig, currentScope));
1150
1201
  if (!state) {
@@ -1160,39 +1211,48 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1160
1211
  details: buildAutoResumeDetails(state),
1161
1212
  };
1162
1213
  }
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
- };
1214
+ const resumeLookup = lookupInteractiveFlowResume(flowEntry, currentScope);
1215
+ return resumeLookup;
1175
1216
  },
1176
1217
  onRun: async (flowId, launchMode) => {
1218
+ const abortController = new AbortController();
1219
+ activeAbortController = abortController;
1220
+ activeFlowId = flowId;
1177
1221
  try {
1178
- const previousScopeType = currentScope.scopeType;
1222
+ const flowEntry = findCatalogEntry(flowId, flowCatalog);
1223
+ if (!flowEntry) {
1224
+ throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1225
+ }
1179
1226
  const previousScopeKey = currentScope.scopeKey;
1180
1227
  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 } : {}),
1228
+ ...(currentScope.jiraRef ? { jiraRef: currentScope.jiraRef } : {}),
1229
+ scopeName: currentScope.scopeKey,
1186
1230
  });
1187
- const nextScope = await resolveScopeForCommand(baseConfig, (form) => ui.requestUserInput(form));
1188
- currentScope = nextScope;
1189
- ui.setScope(currentScope.scopeKey, currentScope.scopeType === "task" ? currentScope.jiraIssueKey : null);
1190
- if (currentScope.scopeType === "task" && (previousScopeType !== "task" || previousScopeKey !== currentScope.scopeKey)) {
1231
+ if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1232
+ const nextScope = await resolveScopeForCommand(baseConfig, (form) => ui.requestUserInput(form));
1233
+ currentScope = nextScope;
1234
+ }
1235
+ else if (flowRequiresTaskScope(flowEntry) && !currentScope.jiraRef) {
1236
+ const jiraContext = await requestJiraContext((form) => ui.requestUserInput(form));
1237
+ currentScope = attachJiraContext(currentScope, jiraContext.jiraRef);
1238
+ }
1239
+ ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null);
1240
+ if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
1191
1241
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1192
1242
  }
1193
- await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode);
1243
+ if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1244
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, createRuntimeServices(abortController.signal));
1245
+ return;
1246
+ }
1247
+ const runtimeConfig = buildRuntimeConfig(baseConfig, currentScope);
1248
+ await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh), (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1194
1249
  }
1195
1250
  catch (error) {
1251
+ if (error instanceof FlowInterruptedError) {
1252
+ ui.appendLog(`[interrupt] ${error.message}`);
1253
+ printInfo(error.message);
1254
+ return;
1255
+ }
1196
1256
  if (error instanceof TaskRunnerError) {
1197
1257
  ui.setFlowFailed(flowId);
1198
1258
  printError(error.message);
@@ -1206,6 +1266,19 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1206
1266
  }
1207
1267
  throw error;
1208
1268
  }
1269
+ finally {
1270
+ if (activeAbortController === abortController) {
1271
+ activeAbortController = null;
1272
+ activeFlowId = null;
1273
+ }
1274
+ }
1275
+ },
1276
+ onInterrupt: async (flowId) => {
1277
+ if (!activeAbortController || activeFlowId !== flowId) {
1278
+ return;
1279
+ }
1280
+ ui.interruptActiveForm();
1281
+ activeAbortController.abort();
1209
1282
  },
1210
1283
  onExit: () => {
1211
1284
  exiting = true;
@@ -1214,12 +1287,10 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1214
1287
  ui.mount();
1215
1288
  printInfo(`Interactive mode for ${currentScope.scopeKey}`);
1216
1289
  printInfo("Use h to see help.");
1217
- if (currentScope.scopeType !== "task") {
1290
+ if (!currentScope.jiraIssueKey) {
1218
1291
  ui.appendLog("[scope] project scope active; task summary will appear after a Jira-backed flow runs");
1219
1292
  }
1220
- if (currentScope.scopeType === "task") {
1221
- syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1222
- }
1293
+ syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1223
1294
  return await new Promise((resolve, reject) => {
1224
1295
  const interval = setInterval(() => {
1225
1296
  if (!exiting) {