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.
- package/README.md +71 -25
- package/dist/artifacts.js +25 -1
- package/dist/errors.js +7 -0
- package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
- package/dist/executors/configs/opencode-config.js +6 -0
- package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
- package/dist/executors/jira-fetch-executor.js +8 -2
- package/dist/executors/opencode-executor.js +35 -0
- package/dist/gitlab.js +199 -5
- package/dist/index.js +215 -144
- package/dist/interactive-ui.js +363 -37
- package/dist/jira.js +116 -14
- package/dist/pipeline/auto-flow.js +1 -1
- package/dist/pipeline/declarative-flows.js +44 -6
- package/dist/pipeline/flow-catalog.js +73 -0
- package/dist/pipeline/flow-specs/auto.json +183 -1
- package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
- package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
- package/dist/pipeline/flow-specs/opencode/auto-opencode.json +1365 -0
- package/dist/pipeline/flow-specs/opencode/bugz/bug-analyze-opencode.json +382 -0
- package/dist/pipeline/flow-specs/opencode/bugz/bug-fix-opencode.json +56 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-diff-review-opencode.json +308 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-review-opencode.json +437 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/mr-description-opencode.json +117 -0
- package/dist/pipeline/flow-specs/opencode/go/run-go-linter-loop-opencode.json +321 -0
- package/dist/pipeline/flow-specs/opencode/go/run-go-tests-loop-opencode.json +321 -0
- package/dist/pipeline/flow-specs/opencode/implement-opencode.json +64 -0
- package/dist/pipeline/flow-specs/opencode/plan-opencode.json +603 -0
- package/dist/pipeline/flow-specs/opencode/review/review-fix-opencode.json +209 -0
- package/dist/pipeline/flow-specs/opencode/review/review-opencode.json +452 -0
- package/dist/pipeline/flow-specs/opencode/task-describe-opencode.json +148 -0
- package/dist/pipeline/flow-specs/plan.json +183 -1
- package/dist/pipeline/node-registry.js +80 -8
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
- package/dist/pipeline/nodes/flow-run-node.js +2 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
- package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
- package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
- package/dist/pipeline/nodes/user-input-node.js +9 -1
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/registry.js +10 -0
- package/dist/pipeline/spec-loader.js +48 -3
- package/dist/pipeline/spec-types.js +43 -1
- package/dist/pipeline/spec-validator.js +53 -7
- package/dist/pipeline/value-resolver.js +15 -1
- package/dist/prompts.js +47 -12
- package/dist/runtime/process-runner.js +45 -1
- package/dist/scope.js +24 -32
- package/dist/structured-artifact-schemas.json +154 -1
- package/dist/structured-artifacts.js +2 -0
- package/dist/user-input.js +7 -0
- package/package.json +1 -1
- package/dist/pipeline/flow-specs/preflight.json +0 -206
- package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
- 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
|
|
3
|
+
`AgentWeaver` is a TypeScript/Node.js CLI for harness engineering around coding agents.
|
|
4
4
|
|
|
5
|
-
It
|
|
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
|
|
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
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
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
|
|
27
|
-
- `src/executors/` contains first-class executors for external actions such as Jira
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
180
|
-
-
|
|
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
|
|
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
|
|
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,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 {
|
|
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
|
-
|
|
41
|
-
const
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|