agentweaver 0.1.9 → 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 (26) hide show
  1. package/README.md +60 -28
  2. package/dist/artifacts.js +1 -1
  3. package/dist/errors.js +7 -0
  4. package/dist/index.js +66 -34
  5. package/dist/interactive-ui.js +351 -44
  6. package/dist/pipeline/declarative-flows.js +7 -4
  7. package/dist/pipeline/flow-catalog.js +28 -21
  8. package/dist/pipeline/flow-specs/opencode/auto-opencode.json +1365 -0
  9. package/dist/pipeline/flow-specs/opencode/bugz/bug-analyze-opencode.json +382 -0
  10. package/dist/pipeline/flow-specs/opencode/bugz/bug-fix-opencode.json +56 -0
  11. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-diff-review-opencode.json +308 -0
  12. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-review-opencode.json +437 -0
  13. package/dist/pipeline/flow-specs/opencode/gitlab/mr-description-opencode.json +117 -0
  14. package/dist/pipeline/flow-specs/opencode/go/run-go-linter-loop-opencode.json +321 -0
  15. package/dist/pipeline/flow-specs/opencode/go/run-go-tests-loop-opencode.json +321 -0
  16. package/dist/pipeline/flow-specs/opencode/implement-opencode.json +64 -0
  17. package/dist/pipeline/flow-specs/{plan-opencode.json → opencode/plan-opencode.json} +4 -4
  18. package/dist/pipeline/flow-specs/opencode/review/review-fix-opencode.json +209 -0
  19. package/dist/pipeline/flow-specs/opencode/review/review-opencode.json +452 -0
  20. package/dist/pipeline/flow-specs/opencode/task-describe-opencode.json +148 -0
  21. package/dist/pipeline/spec-loader.js +18 -7
  22. package/dist/runtime/process-runner.js +45 -1
  23. package/package.json +1 -1
  24. package/dist/pipeline/flow-specs/preflight.json +0 -206
  25. package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
  26. package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
package/README.md CHANGED
@@ -1,37 +1,53 @@
1
1
  # AgentWeaver
2
2
 
3
- `AgentWeaver` is a TypeScript/Node.js CLI for engineering workflows around Jira, GitLab review artifacts, Codex, and Claude.
3
+ `AgentWeaver` is a TypeScript/Node.js CLI for harness engineering around coding agents.
4
4
 
5
- It orchestrates a flow like:
5
+ It brings Jira context, GitLab review artifacts, agent-driven steps via Codex and Claude, an interactive terminal UI, and fully automated workflows into one controlled execution harness.
6
+
7
+ A typical flow looks like:
6
8
 
7
9
  `plan -> implement -> run-go-linter-loop -> run-go-tests-loop -> review -> review-fix`
8
10
 
9
- The package is designed to run as an npm CLI and includes an interactive terminal UI built on `neo-blessed`.
11
+ The point is not the specific chain above, but that `AgentWeaver` lets you design, run, and reuse agent harnesses:
12
+
13
+ - with declarative flows and isolated executors
14
+ - with artifacts that survive restarts and let runs resume from the right point
15
+ - with a TUI for semi-automatic operation and visibility
16
+ - with an `auto` mode for fully automated flows without manual handoff
17
+
18
+ The package runs as an npm CLI and includes a full-screen TUI built on `neo-blessed`.
10
19
 
11
20
  ## What It Does
12
21
 
13
- - Fetches a Jira issue by key or browse URL
14
- - Fetches GitLab merge request review comments into reusable markdown and JSON artifacts
15
- - Fetches GitLab merge request diffs into reusable markdown and JSON artifacts and can run Claude-based diff review directly from MR
16
- - Generates workflow artifacts such as design, implementation plan, QA plan, bug analysis, reviews, and summaries
17
- - Machine-readable JSON artifacts are stored under `.agentweaver/scopes/<scope-key>/.artifacts/` and act as the source of truth between workflow steps; Markdown artifacts remain for human inspection
18
- - Workflow artifacts are isolated by scope; for Jira-driven flows the scope key defaults to the Jira task key, otherwise it defaults to `<git-branch>--<worktree-hash>`
19
- - Runs workflow stages like `bug-analyze`, `bug-fix`, `gitlab-diff-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
20
- - Persists compact `auto` pipeline state on disk so runs can resume without storing large agent outputs
22
+ - Fetches a Jira issue by key or browse URL and turns it into working context for agent steps
23
+ - Fetches GitLab review comments and diffs into reusable Markdown and JSON artifacts
24
+ - Runs agent stages such as `plan`, `implement`, `review`, and `review-fix`, plus verification loops such as `run-go-tests-loop` and `run-go-linter-loop`
25
+ - Stores machine-readable JSON artifacts under `.agentweaver/scopes/<scope-key>/.artifacts/` and uses them as the source of truth between steps
26
+ - Isolates workflows by scope: for Jira-backed runs this is usually the issue key, otherwise it defaults to `<git-branch>--<worktree-hash>`
27
+ - Persists compact `auto` pipeline state on disk so runs can resume without keeping full agent transcripts
21
28
  - Uses Docker runtime services for isolated Codex execution and build verification
22
29
 
30
+ In short, `AgentWeaver` is for cases where you do not want a one-off LLM script, but a durable engineering harness around agents.
31
+
32
+ ## Why AgentWeaver
33
+
34
+ - Harness engineering instead of ad-hoc prompting. Flows, executors, prompts, and artifacts are separate layers rather than one mixed script.
35
+ - Agent runtime instead of single-shot calls. You can build sequences where one agent plans, another implements, and the next verifies and fixes.
36
+ - TUI instead of blind shell execution. The terminal UI gives you an operational view of flow state, activity, and artifacts.
37
+ - Full automation instead of manual step switching. `auto` can run end-to-end flows that move through planning, implementation, verification, and review on their own.
38
+
23
39
  ## Architecture
24
40
 
25
- The CLI now uses an executor + node + declarative flow architecture.
41
+ The CLI is built around an `executor + node + declarative flow` architecture that fits harness engineering well.
26
42
 
27
- - `src/index.ts` remains the CLI entrypoint and high-level orchestration layer
28
- - `src/executors/` contains first-class executors for external actions such as Jira fetch, GitLab review fetch, local Codex, Docker-based build verification, Claude, Claude summaries, and process execution
43
+ - `src/index.ts` remains the CLI entrypoint and top-level orchestration layer
44
+ - `src/executors/` contains first-class executors for external actions such as Jira, GitLab, local Codex, Docker-based build verification, Claude, and process execution
29
45
  - `src/pipeline/nodes/` contains reusable runtime nodes built on top of executors
30
46
  - `src/pipeline/flow-specs/` contains declarative JSON flow specs for `preflight`, `bug-analyze`, `bug-fix`, `gitlab-diff-review`, `gitlab-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
31
- - project-local flow may additionally be placed in `.agentweaver/.flows/*.json`; they are discovered at runtime from the current workspace
32
- - `src/runtime/` contains shared runtime services such as command resolution, Docker runtime environment setup, and subprocess execution
47
+ - project-local flows can be added under `.agentweaver/.flows/*.json`; they are discovered from the current workspace at runtime
48
+ - `src/runtime/` contains shared runtime services such as command resolution, Docker runtime setup, and subprocess execution
33
49
 
34
- This keeps command handlers focused on choosing a flow and providing parameters instead of assembling prompts and subprocess wiring inline.
50
+ This keeps command handlers focused on selecting flows and passing parameters instead of assembling prompts, subprocess wiring, and side effects inline.
35
51
 
36
52
  ## Repository Layout
37
53
 
@@ -118,6 +134,12 @@ GIT_ALLOW_PROTOCOL=file:https:ssh
118
134
 
119
135
  ## Usage
120
136
 
137
+ Primary usage modes:
138
+
139
+ - direct execution of individual stages for controlled agent work
140
+ - interactive TUI mode for selecting flows and observing progress
141
+ - fully automated `auto` mode for end-to-end pipelines
142
+
121
143
  Direct CLI usage:
122
144
 
123
145
  ```bash
@@ -179,18 +201,28 @@ agentweaver auto-reset DEMO-3288
179
201
  Notes:
180
202
 
181
203
  - `--verbose` streams child process `stdout/stderr` in direct CLI mode
182
- - task-only commands such as `plan` and `auto` ask for Jira task via interactive `user-input` when it is omitted
183
- - scope-flexible commands such as `gitlab-diff-review`, `gitlab-review`, `review`, `review-fix`, `run-go-tests-loop`, and `run-go-linter-loop` use the current git branch by default when Jira task is omitted
184
- - `gitlab-review` and `gitlab-diff-review` ask for GitLab merge request URL via interactive `user-input`
185
- - `--scope <name>` lets you override the default project scope name
186
- - the interactive `Activity` pane is intentionally structured: it shows launch separators, prompts, summaries, and short status messages instead of raw Codex/Claude logs by default
204
+ - task-only commands such as `plan` and `auto` ask for a Jira task via interactive `user-input` when it is omitted
205
+ - scope-flexible commands such as `gitlab-diff-review`, `gitlab-review`, `review`, `review-fix`, `run-go-tests-loop`, and `run-go-linter-loop` use the current git branch by default when a Jira task is omitted
206
+ - `gitlab-review` and `gitlab-diff-review` ask for a GitLab merge request URL via interactive `user-input`
207
+ - `--scope <name>` lets you override the default workflow scope name
208
+ - the interactive `Activity` pane intentionally shows structured events, prompts, summaries, and short statuses instead of raw Codex/Claude logs by default
209
+
210
+ For fully automated flows, the main entrypoint looks like:
211
+
212
+ ```bash
213
+ agentweaver auto DEMO-3288
214
+ agentweaver auto-status DEMO-3288
215
+ agentweaver auto-reset DEMO-3288
216
+ ```
217
+
218
+ This lets you run an agent pipeline as a reproducible process rather than a loose set of manual steps.
187
219
 
188
220
  ## Interactive TUI
189
221
 
190
- Interactive mode opens a full-screen terminal UI with:
222
+ Interactive mode opens a full-screen TUI that works as an operator console for the agent harness:
191
223
 
192
224
  - flow list
193
- - current flow progress
225
+ - current progress for the selected flow
194
226
  - activity log
195
227
  - task summary pane
196
228
  - keyboard navigation between panes
@@ -205,11 +237,11 @@ Current navigation:
205
237
 
206
238
  Flow discovery and highlighting:
207
239
 
208
- - built-in flow are loaded from the packaged `src/pipeline/flow-specs/`
209
- - project-local flow are loaded from `.agentweaver/.flows/*.json`
210
- - project-local flow are shown in a different color in the `Flows` pane
240
+ - built-in flows are loaded from `src/pipeline/flow-specs/`
241
+ - project-local flows are loaded from `.agentweaver/.flows/*.json`
242
+ - project-local flows are shown in a different color in the `Flows` pane
211
243
  - when a project-local flow is selected, the description pane also shows its source file path
212
- - if a local flow conflicts with a built-in flow id or uses unknown node / executor / prompt / schema types, interactive startup fails fast with a validation error
244
+ - if a local flow conflicts with a built-in flow id or uses unknown `node` / `executor` / `prompt` / `schema` types, interactive startup fails fast with a validation error
213
245
 
214
246
  Activity pane behavior:
215
247
 
package/dist/artifacts.js CHANGED
@@ -144,7 +144,7 @@ export function autoStateFile(taskKey) {
144
144
  return taskArtifactsFile(taskKey, `.agentweaver-state-${taskKey}.json`);
145
145
  }
146
146
  export function flowStateFile(scopeKey, flowId) {
147
- return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${flowId}.json`);
147
+ return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${encodeURIComponent(flowId)}.json`);
148
148
  }
149
149
  export function planArtifacts(taskKey) {
150
150
  return [designFile(taskKey), designJsonFile(taskKey), planFile(taskKey), planJsonFile(taskKey), qaFile(taskKey), qaJsonFile(taskKey)];
package/dist/errors.js CHANGED
@@ -4,3 +4,10 @@ export class TaskRunnerError extends Error {
4
4
  this.name = "TaskRunnerError";
5
5
  }
6
6
  }
7
+ export class FlowInterruptedError extends TaskRunnerError {
8
+ returnCode = 130;
9
+ constructor(message = "Flow interrupted by user.") {
10
+ super(message);
11
+ this.name = "FlowInterruptedError";
12
+ }
13
+ }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
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 { TaskRunnerError } from "./errors.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";
@@ -41,12 +41,15 @@ const COMMANDS = [
41
41
  const AUTO_STATE_SCHEMA_VERSION = 3;
42
42
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
43
43
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
44
- const runtimeServices = {
45
- resolveCmd,
46
- resolveDockerComposeCmd,
47
- dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
48
- runCommand,
49
- };
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();
50
53
  function buildFailureOutputPreview(output) {
51
54
  const normalized = stripAnsi(output).replace(/\r\n/g, "\n").trim();
52
55
  if (!normalized) {
@@ -104,7 +107,7 @@ function usage() {
104
107
  Interactive Mode:
105
108
  When started without a command, the script opens an interactive UI.
106
109
  If a Jira task is provided, interactive mode starts in the current project scope with Jira context attached.
107
- Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
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.
108
111
 
109
112
  Flags:
110
113
  --version Show package version
@@ -549,6 +552,7 @@ function interactiveFlowDefinition(entry) {
549
552
  label: entry.id,
550
553
  description: flowDescription(entry.id),
551
554
  source: entry.source,
555
+ treePath: [...entry.treePath],
552
556
  ...(entry.source === "project-local" ? { sourcePath: entry.absolutePath } : {}),
553
557
  phases: flow.phases.map((phase) => ({
554
558
  id: phase.id,
@@ -598,13 +602,13 @@ function findCurrentFlowExecutionStep(state) {
598
602
  }
599
603
  return null;
600
604
  }
601
- async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
605
+ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
602
606
  const context = createPipelineContext({
603
607
  issueKey: config.taskKey,
604
608
  jiraRef: config.jiraRef,
605
609
  dryRun: config.dryRun,
606
610
  verbose: config.verbose,
607
- runtime: runtimeServices,
611
+ runtime,
608
612
  ...(setSummary ? { setSummary } : {}),
609
613
  requestUserInput,
610
614
  });
@@ -669,10 +673,12 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, requ
669
673
  throw error;
670
674
  }
671
675
  }
672
- async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
673
- await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, flowParams, requestUserInput, setSummary, launchMode);
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);
674
678
  }
675
679
  function defaultDeclarativeFlowParams(config, forceRefreshSummary = false) {
680
+ const iteration = nextReviewIterationForTask(config.taskKey);
681
+ const latestIteration = latestReviewReplyIteration(config.taskKey);
676
682
  return {
677
683
  taskKey: config.taskKey,
678
684
  jiraRef: config.jiraRef,
@@ -686,6 +692,9 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false) {
686
692
  runGoCoverageScript: config.runGoCoverageScript,
687
693
  extraPrompt: config.extraPrompt,
688
694
  reviewFixPoints: config.reviewFixPoints,
695
+ iteration,
696
+ latestIteration,
697
+ ...(latestIteration !== null ? { reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration) } : {}),
689
698
  forceRefresh: forceRefreshSummary,
690
699
  };
691
700
  }
@@ -710,13 +719,13 @@ function flowRequiresTaskScope(entry) {
710
719
  }
711
720
  return valueReferencesTaskScopeParams(entry.flow.phases);
712
721
  }
713
- async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false) {
722
+ async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
714
723
  const context = createPipelineContext({
715
724
  issueKey: config.taskKey,
716
725
  jiraRef: config.jiraRef,
717
726
  dryRun: config.dryRun,
718
727
  verbose: config.verbose,
719
- runtime: runtimeServices,
728
+ runtime,
720
729
  ...(setSummary ? { setSummary } : {}),
721
730
  requestUserInput: requestUserInputInTerminal,
722
731
  });
@@ -801,13 +810,13 @@ function requireJiraConfig(config) {
801
810
  throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
802
811
  }
803
812
  }
804
- 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) {
805
814
  const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
806
815
  if (config.command === "auto") {
807
816
  if (launchMode === "restart") {
808
817
  resetAutoPipelineState(config);
809
818
  }
810
- await runAutoPipeline(config, setSummary, forceRefreshSummary);
819
+ await runAutoPipeline(config, setSummary, forceRefreshSummary, runtime);
811
820
  return false;
812
821
  }
813
822
  if (config.command === "auto-status") {
@@ -847,7 +856,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
847
856
  taskKey: config.taskKey,
848
857
  extraPrompt: config.extraPrompt,
849
858
  forceRefresh: forceRefreshSummary,
850
- }, requestUserInput, setSummary, launchMode);
859
+ }, requestUserInput, setSummary, launchMode, runtime);
851
860
  return false;
852
861
  }
853
862
  if (config.command === "bug-analyze") {
@@ -862,7 +871,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
862
871
  taskKey: config.taskKey,
863
872
  extraPrompt: config.extraPrompt,
864
873
  forceRefresh: forceRefreshSummary,
865
- }, requestUserInput, setSummary, launchMode);
874
+ }, requestUserInput, setSummary, launchMode, runtime);
866
875
  return false;
867
876
  }
868
877
  if (config.command === "gitlab-review") {
@@ -871,7 +880,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
871
880
  taskKey: config.taskKey,
872
881
  iteration,
873
882
  extraPrompt: config.extraPrompt,
874
- }, requestUserInput, undefined, launchMode);
883
+ }, requestUserInput, undefined, launchMode, runtime);
875
884
  if (!config.dryRun) {
876
885
  printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}`);
877
886
  }
@@ -883,7 +892,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
883
892
  taskKey: config.taskKey,
884
893
  iteration,
885
894
  extraPrompt: config.extraPrompt,
886
- }, requestUserInput, undefined, launchMode);
895
+ }, requestUserInput, undefined, launchMode, runtime);
887
896
  if (!config.dryRun) {
888
897
  printSummary("GitLab Diff Review", `Artifacts:\n${gitlabDiffFile(config.taskKey)}\n${gitlabDiffJsonFile(config.taskKey)}\n${reviewFile(config.taskKey, iteration)}\n${reviewJsonFile(config.taskKey, iteration)}`);
889
898
  }
@@ -901,7 +910,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
901
910
  await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
902
911
  taskKey: config.taskKey,
903
912
  extraPrompt: config.extraPrompt,
904
- }, requestUserInput, undefined, launchMode);
913
+ }, requestUserInput, undefined, launchMode, runtime);
905
914
  return false;
906
915
  }
907
916
  if (config.command === "mr-description") {
@@ -910,7 +919,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
910
919
  await runDeclarativeFlowBySpecFile("mr-description.json", config, {
911
920
  taskKey: config.taskKey,
912
921
  extraPrompt: config.extraPrompt,
913
- }, requestUserInput, undefined, launchMode);
922
+ }, requestUserInput, undefined, launchMode, runtime);
914
923
  return false;
915
924
  }
916
925
  if (config.command === "task-describe") {
@@ -919,7 +928,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
919
928
  jiraApiUrl: config.jiraApiUrl,
920
929
  taskKey: config.taskKey,
921
930
  extraPrompt: config.extraPrompt,
922
- }, requestUserInput, undefined, launchMode);
931
+ }, requestUserInput, undefined, launchMode, runtime);
923
932
  return false;
924
933
  }
925
934
  if (config.command === "implement") {
@@ -947,14 +956,14 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
947
956
  taskKey: config.taskKey,
948
957
  iteration,
949
958
  extraPrompt: config.extraPrompt,
950
- }, requestUserInput, undefined, launchMode);
959
+ }, requestUserInput, undefined, launchMode, runtime);
951
960
  }
952
961
  else {
953
962
  await runDeclarativeFlowBySpecFile("review-project.json", config, {
954
963
  taskKey: config.taskKey,
955
964
  iteration,
956
965
  extraPrompt: config.extraPrompt,
957
- }, requestUserInput, undefined, launchMode);
966
+ }, requestUserInput, undefined, launchMode, runtime);
958
967
  }
959
968
  return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
960
969
  }
@@ -973,7 +982,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
973
982
  reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
974
983
  extraPrompt: config.extraPrompt,
975
984
  reviewFixPoints: config.reviewFixPoints,
976
- }, requestUserInput, undefined, launchMode);
985
+ }, requestUserInput, undefined, launchMode, runtime);
977
986
  return false;
978
987
  }
979
988
  if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
@@ -982,12 +991,12 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
982
991
  runGoTestsScript: config.runGoTestsScript,
983
992
  runGoLinterScript: config.runGoLinterScript,
984
993
  extraPrompt: config.extraPrompt,
985
- }, requestUserInput, undefined, launchMode);
994
+ }, requestUserInput, undefined, launchMode, runtime);
986
995
  return false;
987
996
  }
988
997
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
989
998
  }
990
- async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false) {
999
+ async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
991
1000
  checkAutoPrerequisites(config);
992
1001
  printInfo("Dry-run auto pipeline from declarative spec");
993
1002
  const autoFlow = loadAutoFlow();
@@ -1000,16 +1009,16 @@ async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = f
1000
1009
  publishFlowState("auto", executionState);
1001
1010
  for (const phase of autoFlow.phases) {
1002
1011
  printInfo(`Dry-run auto phase: ${phase.id}`);
1003
- await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary);
1012
+ await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary, runtime);
1004
1013
  if (executionState.terminated) {
1005
1014
  break;
1006
1015
  }
1007
1016
  }
1008
1017
  }
1009
- async function runAutoPipeline(config, setSummary, forceRefreshSummary = false) {
1018
+ async function runAutoPipeline(config, setSummary, forceRefreshSummary = false, runtime = runtimeServices) {
1010
1019
  requireJiraConfig(config);
1011
1020
  if (config.dryRun) {
1012
- await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary);
1021
+ await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary, runtime);
1013
1022
  return;
1014
1023
  }
1015
1024
  checkAutoPrerequisites(config);
@@ -1049,7 +1058,7 @@ async function runAutoPipeline(config, setSummary, forceRefreshSummary = false)
1049
1058
  saveAutoPipelineState(state);
1050
1059
  try {
1051
1060
  printInfo(`Running auto step: ${step.id}`);
1052
- 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);
1053
1062
  step.status = status;
1054
1063
  step.finishedAt = nowIso8601();
1055
1064
  step.returnCode = 0;
@@ -1165,6 +1174,8 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1165
1174
  let currentScope = resolveProjectScope(scopeName, jiraRef);
1166
1175
  const gitBranchName = detectGitBranchName();
1167
1176
  const flowCatalog = loadInteractiveFlowCatalog(process.cwd());
1177
+ let activeAbortController = null;
1178
+ let activeFlowId = null;
1168
1179
  let exiting = false;
1169
1180
  const ui = new InteractiveUi({
1170
1181
  scopeKey: currentScope.scopeKey,
@@ -1204,6 +1215,9 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1204
1215
  return resumeLookup;
1205
1216
  },
1206
1217
  onRun: async (flowId, launchMode) => {
1218
+ const abortController = new AbortController();
1219
+ activeAbortController = abortController;
1220
+ activeFlowId = flowId;
1207
1221
  try {
1208
1222
  const flowEntry = findCatalogEntry(flowId, flowCatalog);
1209
1223
  if (!flowEntry) {
@@ -1227,13 +1241,18 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1227
1241
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1228
1242
  }
1229
1243
  if (flowEntry.source === "built-in" && isBuiltInCommandFlowId(flowId)) {
1230
- await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode);
1244
+ await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode, createRuntimeServices(abortController.signal));
1231
1245
  return;
1232
1246
  }
1233
1247
  const runtimeConfig = buildRuntimeConfig(baseConfig, currentScope);
1234
- await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh), (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode);
1248
+ await runDeclarativeFlowByRef(flowId, toDeclarativeFlowRef(flowEntry), runtimeConfig, defaultDeclarativeFlowParams(runtimeConfig, forceRefresh), (form) => ui.requestUserInput(form), (markdown) => ui.setSummary(markdown), launchMode, createRuntimeServices(abortController.signal));
1235
1249
  }
1236
1250
  catch (error) {
1251
+ if (error instanceof FlowInterruptedError) {
1252
+ ui.appendLog(`[interrupt] ${error.message}`);
1253
+ printInfo(error.message);
1254
+ return;
1255
+ }
1237
1256
  if (error instanceof TaskRunnerError) {
1238
1257
  ui.setFlowFailed(flowId);
1239
1258
  printError(error.message);
@@ -1247,6 +1266,19 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1247
1266
  }
1248
1267
  throw error;
1249
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();
1250
1282
  },
1251
1283
  onExit: () => {
1252
1284
  exiting = true;