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/README.md CHANGED
@@ -1,41 +1,60 @@
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
- - Generates workflow artifacts such as design, implementation plan, QA plan, bug analysis, reviews, and summaries
16
- - 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
17
- - 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>`
18
- - Runs workflow stages like `bug-analyze`, `bug-fix`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
19
- - 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
20
28
  - Uses Docker runtime services for isolated Codex execution and build verification
21
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
+
22
39
  ## Architecture
23
40
 
24
- 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.
25
42
 
26
- - `src/index.ts` remains the CLI entrypoint and high-level orchestration layer
27
- - `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
28
45
  - `src/pipeline/nodes/` contains reusable runtime nodes built on top of executors
29
- - `src/pipeline/flow-specs/` contains declarative JSON flow specs for `preflight`, `bug-analyze`, `bug-fix`, `gitlab-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
30
- - `src/runtime/` contains shared runtime services such as command resolution, Docker runtime environment setup, and subprocess execution
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`
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
31
49
 
32
- 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.
33
51
 
34
52
  ## Repository Layout
35
53
 
36
54
  - `src/` — main TypeScript sources
37
55
  - `src/index.ts` — CLI entrypoint and workflow orchestration
38
56
  - `src/pipeline/flow-specs/` — declarative JSON specs for workflow stages
57
+ - `.agentweaver/.flows/` — optional project-local declarative flow specs loaded from the current repository
39
58
  - `src/pipeline/nodes/` — reusable pipeline nodes executed by the declarative runner
40
59
  - `src/interactive-ui.ts` — interactive TUI built with `neo-blessed`
41
60
  - `src/markdown.ts` — markdown-to-terminal renderer for the TUI
@@ -89,7 +108,7 @@ Required:
89
108
  Common optional variables:
90
109
 
91
110
  - `JIRA_BASE_URL` — required when you pass only an issue key like `DEMO-123`
92
- - `GITLAB_TOKEN` — personal access token for `gitlab-review`
111
+ - `GITLAB_TOKEN` — personal access token for `gitlab-review` and `gitlab-diff-review`
93
112
  - `AGENTWEAVER_HOME` — path to the AgentWeaver installation directory
94
113
  - `DOCKER_COMPOSE_BIN` — override compose command, for example `docker compose`
95
114
  - `CODEX_BIN` — override `codex` executable path
@@ -115,6 +134,12 @@ GIT_ALLOW_PROTOCOL=file:https:ssh
115
134
 
116
135
  ## Usage
117
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
+
118
143
  Direct CLI usage:
119
144
 
120
145
  ```bash
@@ -122,7 +147,8 @@ agentweaver plan DEMO-3288
122
147
  agentweaver plan
123
148
  agentweaver bug-analyze DEMO-3288
124
149
  agentweaver bug-fix DEMO-3288
125
- agentweaver gitlab-review DEMO-3288
150
+ agentweaver gitlab-diff-review
151
+ agentweaver gitlab-review
126
152
  agentweaver mr-description DEMO-3288
127
153
  agentweaver task-describe DEMO-3288
128
154
  agentweaver implement DEMO-3288
@@ -142,7 +168,8 @@ node dist/index.js plan DEMO-3288
142
168
  node dist/index.js plan
143
169
  node dist/index.js bug-analyze DEMO-3288
144
170
  node dist/index.js bug-fix DEMO-3288
145
- node dist/index.js gitlab-review DEMO-3288
171
+ node dist/index.js gitlab-diff-review
172
+ node dist/index.js gitlab-review
146
173
  node dist/index.js mr-description DEMO-3288
147
174
  node dist/index.js task-describe DEMO-3288
148
175
  node dist/index.js review
@@ -174,17 +201,28 @@ agentweaver auto-reset DEMO-3288
174
201
  Notes:
175
202
 
176
203
  - `--verbose` streams child process `stdout/stderr` in direct CLI mode
177
- - task-only commands such as `plan` and `auto` ask for Jira task via interactive `user-input` when it is omitted
178
- - scope-flexible commands such as `review`, `review-fix`, `run-go-tests-loop`, and `run-go-linter-loop` use the current git branch by default when Jira task is omitted
179
- - `--scope <name>` lets you override the default project scope name
180
- - 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.
181
219
 
182
220
  ## Interactive TUI
183
221
 
184
- 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:
185
223
 
186
224
  - flow list
187
- - current flow progress
225
+ - current progress for the selected flow
188
226
  - activity log
189
227
  - task summary pane
190
228
  - keyboard navigation between panes
@@ -197,6 +235,14 @@ Current navigation:
197
235
  - `h` — help overlay
198
236
  - `q` or `Ctrl+C` — exit
199
237
 
238
+ Flow discovery and highlighting:
239
+
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
243
+ - when a project-local flow is selected, the description pane also shows its source file path
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
245
+
200
246
  Activity pane behavior:
201
247
 
202
248
  - each external launch is separated with a framed block that shows the current `node`, `executor`, and `model` when available
package/dist/artifacts.js CHANGED
@@ -59,6 +59,12 @@ export function planFile(taskKey) {
59
59
  export function planJsonFile(taskKey) {
60
60
  return artifactJsonFile("plan", taskKey, 1);
61
61
  }
62
+ export function planningQuestionsJsonFile(taskKey) {
63
+ return taskArtifactsFile(taskKey, `planning-questions-${taskKey}.json`);
64
+ }
65
+ export function planningAnswersJsonFile(taskKey) {
66
+ return taskArtifactsFile(taskKey, `planning-answers-${taskKey}.json`);
67
+ }
62
68
  export function bugAnalyzeFile(taskKey) {
63
69
  return taskWorkspaceFile(taskKey, `bug-analyze-${taskKey}.md`);
64
70
  }
@@ -95,6 +101,15 @@ export function readyToMergeFile(taskKey) {
95
101
  export function jiraTaskFile(taskKey) {
96
102
  return taskArtifactsFile(taskKey, `${taskKey}.json`);
97
103
  }
104
+ export function jiraAttachmentsDir(taskKey) {
105
+ return path.join(taskArtifactsDir(taskKey), "jira-attachments");
106
+ }
107
+ export function jiraAttachmentsManifestFile(taskKey) {
108
+ return taskArtifactsFile(taskKey, `jira-attachments-${taskKey}.json`);
109
+ }
110
+ export function jiraAttachmentsContextFile(taskKey) {
111
+ return taskWorkspaceFile(taskKey, `jira-attachments-context-${taskKey}.txt`);
112
+ }
98
113
  export function jiraDescriptionFile(taskKey) {
99
114
  return taskWorkspaceFile(taskKey, `jira-${taskKey}-description.md`);
100
115
  }
@@ -116,11 +131,20 @@ export function gitlabReviewJsonFile(taskKey) {
116
131
  export function gitlabReviewInputJsonFile(taskKey) {
117
132
  return taskArtifactsFile(taskKey, `gitlab-review-input-${taskKey}.json`);
118
133
  }
134
+ export function gitlabDiffFile(taskKey) {
135
+ return taskWorkspaceFile(taskKey, `gitlab-diff-${taskKey}.md`);
136
+ }
137
+ export function gitlabDiffJsonFile(taskKey) {
138
+ return taskArtifactsFile(taskKey, `gitlab-diff-${taskKey}.json`);
139
+ }
140
+ export function gitlabDiffReviewInputJsonFile(taskKey) {
141
+ return taskArtifactsFile(taskKey, `gitlab-diff-review-input-${taskKey}.json`);
142
+ }
119
143
  export function autoStateFile(taskKey) {
120
144
  return taskArtifactsFile(taskKey, `.agentweaver-state-${taskKey}.json`);
121
145
  }
122
146
  export function flowStateFile(scopeKey, flowId) {
123
- return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${flowId}.json`);
147
+ return scopeArtifactsFile(scopeKey, `.agentweaver-flow-state-${encodeURIComponent(flowId)}.json`);
124
148
  }
125
149
  export function planArtifacts(taskKey) {
126
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
+ }
@@ -0,0 +1,3 @@
1
+ export const fetchGitLabDiffExecutorDefaultConfig = {
2
+ authEnvVar: "GITLAB_TOKEN",
3
+ };
@@ -0,0 +1,6 @@
1
+ export const opencodeExecutorDefaultConfig = {
2
+ commandEnvVar: "OPENCODE_BIN",
3
+ defaultCommand: "opencode",
4
+ modelEnvVar: "OPENCODE_MODEL",
5
+ subcommand: "run",
6
+ };
@@ -0,0 +1,26 @@
1
+ import { fetchGitLabDiffExecutorDefaultConfig } from "./configs/fetch-gitlab-diff-config.js";
2
+ import { buildGitLabMergeRequestDiffFetchTarget, fetchGitLabMergeRequestDiff } from "../gitlab.js";
3
+ export const fetchGitLabDiffExecutor = {
4
+ kind: "fetch-gitlab-diff",
5
+ version: 1,
6
+ defaultConfig: fetchGitLabDiffExecutorDefaultConfig,
7
+ async execute(context, input) {
8
+ const target = buildGitLabMergeRequestDiffFetchTarget(input.mergeRequestUrl);
9
+ if (context.verbose) {
10
+ context.ui.writeStdout(`GitLab MR URL: ${target.mergeRequestUrl}\n`);
11
+ context.ui.writeStdout(`GitLab project path: ${target.projectPath}\n`);
12
+ context.ui.writeStdout(`GitLab merge request IID: ${target.mergeRequestIid}\n`);
13
+ context.ui.writeStdout(`GitLab merge request API URL: ${target.mergeRequestApiUrl}\n`);
14
+ context.ui.writeStdout(`GitLab diffs API URL: ${target.diffsApiUrl}\n`);
15
+ context.ui.writeStdout(`Saving GitLab diff markdown to: ${input.outputFile}\n`);
16
+ context.ui.writeStdout(`Saving GitLab diff JSON to: ${input.outputJsonFile}\n`);
17
+ }
18
+ const artifact = await fetchGitLabMergeRequestDiff(input.mergeRequestUrl, input.outputFile, input.outputJsonFile);
19
+ return {
20
+ outputFile: input.outputFile,
21
+ outputJsonFile: input.outputJsonFile,
22
+ mergeRequestUrl: artifact.merge_request_url,
23
+ filesCount: artifact.files.length,
24
+ };
25
+ },
26
+ };
@@ -5,7 +5,13 @@ export const jiraFetchExecutor = {
5
5
  version: 1,
6
6
  defaultConfig: jiraFetchExecutorDefaultConfig,
7
7
  async execute(_context, input) {
8
- await fetchJiraIssue(input.jiraApiUrl, input.outputFile);
9
- return { outputFile: input.outputFile };
8
+ const artifacts = await fetchJiraIssue(input.jiraApiUrl, input.outputFile, input.attachmentsManifestFile, input.attachmentsContextFile);
9
+ return {
10
+ outputFile: artifacts.issueFile,
11
+ downloadedAttachments: artifacts.downloadedAttachments,
12
+ planningContextAttachments: artifacts.planningContextAttachments,
13
+ ...(artifacts.attachmentsManifestFile ? { attachmentsManifestFile: artifacts.attachmentsManifestFile } : {}),
14
+ ...(artifacts.attachmentsContextFile ? { attachmentsContextFile: artifacts.attachmentsContextFile } : {}),
15
+ };
10
16
  },
11
17
  };
@@ -0,0 +1,35 @@
1
+ import { opencodeExecutorDefaultConfig } from "./configs/opencode-config.js";
2
+ import { processExecutor } from "./process-executor.js";
3
+ function resolveModel(config, inputModel, env) {
4
+ const explicitModel = inputModel?.trim();
5
+ if (explicitModel) {
6
+ return explicitModel;
7
+ }
8
+ const envModel = env[config.modelEnvVar]?.trim();
9
+ return envModel || undefined;
10
+ }
11
+ export const opencodeExecutor = {
12
+ kind: "opencode",
13
+ version: 1,
14
+ defaultConfig: opencodeExecutorDefaultConfig,
15
+ async execute(context, input, config) {
16
+ const env = input.env ?? context.env;
17
+ const command = input.command ?? context.runtime.resolveCmd(config.defaultCommand, config.commandEnvVar);
18
+ const model = resolveModel(config, input.model, env);
19
+ const argv = [command, config.subcommand];
20
+ if (model) {
21
+ argv.push("--model", model);
22
+ }
23
+ argv.push(input.prompt);
24
+ const result = await processExecutor.execute(context, {
25
+ argv,
26
+ env,
27
+ label: model ? `opencode:${model}` : "opencode",
28
+ }, processExecutor.defaultConfig);
29
+ return {
30
+ output: result.output,
31
+ command,
32
+ ...(model ? { model } : {}),
33
+ };
34
+ },
35
+ };
package/dist/gitlab.js CHANGED
@@ -37,8 +37,17 @@ export function buildGitLabReviewFetchTarget(mergeRequestUrl) {
37
37
  discussionsApiUrl: `${mergeRequestRef.apiBaseUrl}/projects/${encodeURIComponent(mergeRequestRef.projectPath)}/merge_requests/${mergeRequestRef.mergeRequestIid}/discussions`,
38
38
  };
39
39
  }
40
- async function fetchDiscussionPage(target, page, token) {
41
- const apiUrl = `${target.discussionsApiUrl}?per_page=100&page=${page}`;
40
+ export function buildGitLabMergeRequestDiffFetchTarget(mergeRequestUrl) {
41
+ const mergeRequestRef = parseGitLabMergeRequestUrl(mergeRequestUrl);
42
+ const mergeRequestApiUrl = `${mergeRequestRef.apiBaseUrl}/projects/${encodeURIComponent(mergeRequestRef.projectPath)}` +
43
+ `/merge_requests/${mergeRequestRef.mergeRequestIid}`;
44
+ return {
45
+ ...mergeRequestRef,
46
+ mergeRequestApiUrl,
47
+ diffsApiUrl: `${mergeRequestApiUrl}/diffs`,
48
+ };
49
+ }
50
+ async function fetchGitLabJson(apiUrl, token) {
42
51
  const response = await fetch(apiUrl, {
43
52
  headers: {
44
53
  "PRIVATE-TOKEN": token,
@@ -46,17 +55,31 @@ async function fetchDiscussionPage(target, page, token) {
46
55
  },
47
56
  });
48
57
  if (!response.ok) {
58
+ throw new TaskRunnerError(`GitLab API request failed: HTTP ${response.status}\nURL: ${apiUrl}`);
59
+ }
60
+ return {
61
+ body: (await response.json()),
62
+ headers: response.headers,
63
+ };
64
+ }
65
+ async function fetchDiscussionPage(target, page, token) {
66
+ const apiUrl = `${target.discussionsApiUrl}?per_page=100&page=${page}`;
67
+ let payload;
68
+ try {
69
+ payload = await fetchGitLabJson(apiUrl, token);
70
+ }
71
+ catch (error) {
49
72
  throw new TaskRunnerError([
50
- `Failed to fetch GitLab merge request discussions: HTTP ${response.status}`,
73
+ `Failed to fetch GitLab merge request discussions: ${error.message}`,
51
74
  `MR URL: ${target.mergeRequestUrl}`,
52
75
  `GitLab project path: ${target.projectPath}`,
53
76
  `GitLab merge request IID: ${target.mergeRequestIid}`,
54
77
  `GitLab discussions API URL: ${apiUrl}`,
55
78
  ].join("\n"));
56
79
  }
57
- const nextPageHeader = response.headers.get("x-next-page");
80
+ const nextPageHeader = payload.headers.get("x-next-page");
58
81
  const nextPage = nextPageHeader && nextPageHeader.trim().length > 0 ? Number.parseInt(nextPageHeader, 10) : null;
59
- const discussions = (await response.json());
82
+ const discussions = payload.body;
60
83
  return { discussions, nextPage: Number.isNaN(nextPage ?? Number.NaN) ? null : nextPage };
61
84
  }
62
85
  async function fetchMergeRequestDiscussions(target, token) {
@@ -128,6 +151,142 @@ function buildGitLabReviewMarkdown(artifact) {
128
151
  });
129
152
  return lines.join("\n");
130
153
  }
154
+ function normalizeMergeRequestAuthor(details) {
155
+ return details.author?.username?.trim() || details.author?.name?.trim() || "unknown";
156
+ }
157
+ function normalizeDiffContent(diff, response) {
158
+ const trimmed = diff?.trimEnd() ?? "";
159
+ if (trimmed.length > 0) {
160
+ return trimmed;
161
+ }
162
+ if (response.too_large) {
163
+ return "[Diff omitted by GitLab because it is too large.]";
164
+ }
165
+ if (response.collapsed) {
166
+ return "[Diff omitted by GitLab because it is collapsed.]";
167
+ }
168
+ return "[Diff content is empty.]";
169
+ }
170
+ function normalizeDiffFiles(diffs) {
171
+ return diffs.map((file, index) => {
172
+ const newPath = file.new_path?.trim() || file.old_path?.trim() || `unknown-file-${index + 1}`;
173
+ const oldPath = file.old_path?.trim() || newPath;
174
+ let changeType = "modified";
175
+ if (file.new_file) {
176
+ changeType = "added";
177
+ }
178
+ else if (file.deleted_file) {
179
+ changeType = "deleted";
180
+ }
181
+ else if (file.renamed_file) {
182
+ changeType = "renamed";
183
+ }
184
+ return {
185
+ old_path: oldPath,
186
+ new_path: newPath,
187
+ change_type: changeType,
188
+ new_file: Boolean(file.new_file),
189
+ renamed_file: Boolean(file.renamed_file),
190
+ deleted_file: Boolean(file.deleted_file),
191
+ generated_file: Boolean(file.generated_file),
192
+ too_large: Boolean(file.too_large),
193
+ collapsed: Boolean(file.collapsed),
194
+ diff: normalizeDiffContent(file.diff, file),
195
+ };
196
+ });
197
+ }
198
+ async function fetchMergeRequestDetails(target, token) {
199
+ try {
200
+ return (await fetchGitLabJson(target.mergeRequestApiUrl, token)).body;
201
+ }
202
+ catch (error) {
203
+ throw new TaskRunnerError([
204
+ `Failed to fetch GitLab merge request details: ${error.message}`,
205
+ `MR URL: ${target.mergeRequestUrl}`,
206
+ `GitLab project path: ${target.projectPath}`,
207
+ `GitLab merge request IID: ${target.mergeRequestIid}`,
208
+ `GitLab merge request API URL: ${target.mergeRequestApiUrl}`,
209
+ ].join("\n"));
210
+ }
211
+ }
212
+ async function fetchMergeRequestDiffsPage(target, page, token) {
213
+ const apiUrl = `${target.diffsApiUrl}?per_page=100&page=${page}`;
214
+ let payload;
215
+ try {
216
+ payload = await fetchGitLabJson(apiUrl, token);
217
+ }
218
+ catch (error) {
219
+ throw new TaskRunnerError([
220
+ `Failed to fetch GitLab merge request diffs: ${error.message}`,
221
+ `MR URL: ${target.mergeRequestUrl}`,
222
+ `GitLab project path: ${target.projectPath}`,
223
+ `GitLab merge request IID: ${target.mergeRequestIid}`,
224
+ `GitLab diffs API URL: ${apiUrl}`,
225
+ ].join("\n"));
226
+ }
227
+ const nextPageHeader = payload.headers.get("x-next-page");
228
+ const nextPage = nextPageHeader && nextPageHeader.trim().length > 0 ? Number.parseInt(nextPageHeader, 10) : null;
229
+ return {
230
+ diffs: payload.body,
231
+ nextPage: Number.isNaN(nextPage ?? Number.NaN) ? null : nextPage,
232
+ };
233
+ }
234
+ async function fetchMergeRequestDiffs(target, token) {
235
+ const diffs = [];
236
+ let page = 1;
237
+ while (true) {
238
+ const chunk = await fetchMergeRequestDiffsPage(target, page, token);
239
+ diffs.push(...chunk.diffs);
240
+ if (!chunk.nextPage) {
241
+ return diffs;
242
+ }
243
+ page = chunk.nextPage;
244
+ }
245
+ }
246
+ function buildGitLabMergeRequestDiffMarkdown(artifact) {
247
+ const lines = [
248
+ "# GitLab MR Diff",
249
+ "",
250
+ `- MR: ${artifact.merge_request_url}`,
251
+ `- Title: ${artifact.merge_request.title}`,
252
+ `- Project: ${artifact.project_path}`,
253
+ `- IID: ${artifact.merge_request_iid}`,
254
+ `- State: ${artifact.merge_request.state}`,
255
+ `- Draft: ${artifact.merge_request.draft ? "yes" : "no"}`,
256
+ `- Author: ${artifact.merge_request.author}`,
257
+ `- Branches: ${artifact.merge_request.source_branch} -> ${artifact.merge_request.target_branch}`,
258
+ `- SHA: ${artifact.merge_request.sha}`,
259
+ `- Fetched at: ${artifact.fetched_at}`,
260
+ `- Files changed: ${artifact.files.length}`,
261
+ "",
262
+ ];
263
+ const description = artifact.merge_request.description.trim();
264
+ if (description) {
265
+ lines.push("## Description", "", description, "");
266
+ }
267
+ if (artifact.files.length === 0) {
268
+ lines.push("Изменений в diff не найдено.");
269
+ return lines.join("\n");
270
+ }
271
+ artifact.files.forEach((file, index) => {
272
+ lines.push(`## File ${index + 1}: ${file.new_path}`);
273
+ lines.push(`- Change type: ${file.change_type}`);
274
+ if (file.old_path !== file.new_path) {
275
+ lines.push(`- Old path: ${file.old_path}`);
276
+ }
277
+ if (file.generated_file) {
278
+ lines.push("- Generated: yes");
279
+ }
280
+ if (file.too_large) {
281
+ lines.push("- Too large: yes");
282
+ }
283
+ if (file.collapsed) {
284
+ lines.push("- Collapsed: yes");
285
+ }
286
+ lines.push("", "```diff", file.diff, "```", "");
287
+ });
288
+ return lines.join("\n");
289
+ }
131
290
  export async function fetchGitLabReview(mergeRequestUrl, outputFile, outputJsonFile) {
132
291
  const token = process.env.GITLAB_TOKEN?.trim();
133
292
  if (!token) {
@@ -151,3 +310,38 @@ export async function fetchGitLabReview(mergeRequestUrl, outputFile, outputJsonF
151
310
  await writeFile(outputFile, `${buildGitLabReviewMarkdown(artifact)}\n`, "utf8");
152
311
  return artifact;
153
312
  }
313
+ export async function fetchGitLabMergeRequestDiff(mergeRequestUrl, outputFile, outputJsonFile) {
314
+ const token = process.env.GITLAB_TOKEN?.trim();
315
+ if (!token) {
316
+ throw new TaskRunnerError("GITLAB_TOKEN is required for gitlab-diff-review flow.");
317
+ }
318
+ const target = buildGitLabMergeRequestDiffFetchTarget(mergeRequestUrl);
319
+ const [details, diffs] = await Promise.all([fetchMergeRequestDetails(target, token), fetchMergeRequestDiffs(target, token)]);
320
+ const files = normalizeDiffFiles(diffs);
321
+ const fetchedAt = new Date().toISOString();
322
+ const artifact = {
323
+ summary: files.length > 0 ? `Fetched GitLab MR diff with ${files.length} changed files.` : "GitLab MR diff is empty.",
324
+ merge_request_url: target.mergeRequestUrl,
325
+ project_path: target.projectPath,
326
+ merge_request_iid: target.mergeRequestIid,
327
+ fetched_at: fetchedAt,
328
+ merge_request: {
329
+ title: details.title?.trim() || `MR !${target.mergeRequestIid}`,
330
+ description: details.description?.trim() || "",
331
+ state: details.state?.trim() || "unknown",
332
+ draft: Boolean(details.draft),
333
+ source_branch: details.source_branch?.trim() || "unknown",
334
+ target_branch: details.target_branch?.trim() || "unknown",
335
+ sha: details.sha?.trim() || "unknown",
336
+ author: normalizeMergeRequestAuthor(details),
337
+ created_at: details.created_at ?? new Date(0).toISOString(),
338
+ updated_at: details.updated_at ?? new Date(0).toISOString(),
339
+ },
340
+ files,
341
+ };
342
+ mkdirSync(path.dirname(outputFile), { recursive: true });
343
+ mkdirSync(path.dirname(outputJsonFile), { recursive: true });
344
+ await writeFile(outputJsonFile, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
345
+ await writeFile(outputFile, `${buildGitLabMergeRequestDiffMarkdown(artifact)}\n`, "utf8");
346
+ return artifact;
347
+ }