Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ RUN ARCH=$(dpkg --print-architecture) && \
rm /tmp/ast-grep.zip && \
chmod +x /usr/local/bin/sg

# Install glab (GitLab CLI)
RUN ARCH=$(dpkg --print-architecture) && \
curl -L "https://gitlab.com/gitlab-org/cli/-/releases/v1.52.0/downloads/glab_1.52.0_linux_${ARCH}.deb" -o /tmp/glab.deb && \
dpkg -i /tmp/glab.deb && \
rm /tmp/glab.deb

# Install agent CLIs used by headless engines in worker jobs
RUN npm install -g @anthropic-ai/claude-code @openai/codex@0.114.0 opencode-ai

Expand Down
61 changes: 61 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.91",
"@gitbeaker/rest": "^43.8.0",
"@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.4.2",
"@llmist/cli": "^16.0.3",
Expand Down
163 changes: 163 additions & 0 deletions src/agents/definitions/contextSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* These are the building blocks composed by the YAML contextPipeline arrays.
*/

import { getIntegrationProvider } from '../../db/repositories/credentialsRepository.js';
import { formatCheckStatus } from '../../gadgets/github/core/getPRChecks.js';
import { ListDirectory } from '../../gadgets/ListDirectory.js';
import { readWorkItem, readWorkItemWithMedia } from '../../gadgets/pm/core/readWorkItem.js';
Expand All @@ -17,6 +18,7 @@ import {
saveTodos,
} from '../../gadgets/todo/storage.js';
import { githubClient } from '../../github/client.js';
import { gitlabClient } from '../../gitlab/client.js';
import { getJiraConfig, getTrelloConfig } from '../../pm/config.js';
import { getPMProviderOrNull, MAX_IMAGES_PER_WORK_ITEM } from '../../pm/index.js';
import { getSentryClient } from '../../sentry/client.js';
Expand Down Expand Up @@ -157,6 +159,24 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
if (!repoFullName || !prNumber) {
throw new Error('fetchPRContextStep requires repoFullName and prNumber in input');
}

// Check if the project uses GitLab
const scmProvider = params.project?.id
? await getIntegrationProvider(params.project.id, 'scm')
: null;

if (scmProvider === 'gitlab') {
return fetchGitLabMRContextStep(params, repoFullName, prNumber);
}

return fetchGitHubPRContextStep(params, repoFullName, prNumber);
}

async function fetchGitHubPRContextStep(
params: FetchContextParams,
repoFullName: string,
prNumber: number,
): Promise<ContextInjection[]> {
const injections: ContextInjection[] = [];
const { owner, repo } = parseRepoFullName(repoFullName);

Expand Down Expand Up @@ -215,13 +235,119 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
return injections;
}

async function fetchGitLabMRContextStep(
params: FetchContextParams,
projectPath: string,
mrIid: number,
): Promise<ContextInjection[]> {
const injections: ContextInjection[] = [];

params.logWriter('INFO', 'Fetching MR details and diff from GitLab', {
projectPath,
mrIid,
});

const mrDetails = await gitlabClient.getMR(projectPath, mrIid);
const mrDiff = await gitlabClient.getMRDiff(projectPath, mrIid);

// Format MR details
const detailsFormatted = [
`MR #${mrDetails.iid}: ${mrDetails.title}`,
`State: ${mrDetails.state}`,
`Author: ${mrDetails.author.username}`,
`Source: ${mrDetails.sourceBranch} → Target: ${mrDetails.targetBranch}`,
`URL: ${mrDetails.webUrl}`,
mrDetails.description ? `\nDescription:\n${mrDetails.description}` : '',
]
.filter(Boolean)
.join('\n');

// Format diff
const diffFormatted = mrDiff
.map((f) => {
const status = f.newFile
? 'added'
: f.deletedFile
? 'removed'
: f.renamedFile
? 'renamed'
: 'modified';
return `--- ${f.oldPath}\n+++ ${f.newPath} (${status})\n${f.diff || '(binary or empty)'}`;
})
.join('\n\n');

injections.push({
toolName: 'GetMRDetails',
params: { comment: 'Pre-fetching MR details for review context', projectPath, mrIid },
result: detailsFormatted,
description: 'Pre-fetched MR details',
});

injections.push({
toolName: 'GetMRDiff',
params: { comment: 'Pre-fetching MR diff for code review', projectPath, mrIid },
result: diffFormatted,
description: 'Pre-fetched MR diff',
});

// Read full contents of changed files from the local repo
const prDiffCompat = mrDiff.map((f) => ({
filename: f.newPath,
status: (f.newFile
? 'added'
: f.deletedFile
? 'removed'
: f.renamedFile
? 'renamed'
: 'modified') as
| 'added'
| 'removed'
| 'modified'
| 'renamed'
| 'copied'
| 'changed'
| 'unchanged',
additions: 0,
deletions: 0,
changes: 0,
patch: f.diff,
}));
params.logWriter('INFO', 'Reading MR file contents', { fileCount: mrDiff.length });
const fileContents = await readPRFileContents(params.repoDir, prDiffCompat);
params.logWriter('INFO', 'File contents loaded', {
included: fileContents.included.length,
skipped: fileContents.skipped.length,
});

for (const file of fileContents.included) {
injections.push({
toolName: 'ReadFile',
params: { comment: `Pre-fetching ${file.path} for review`, filePath: file.path },
result: `path=${file.path}\n\n${file.content}`,
description: `Pre-fetched ${file.path}`,
});
}

return injections;
}

export async function fetchPRConversationStep(
params: FetchContextParams,
): Promise<ContextInjection[]> {
const { repoFullName, prNumber } = params.input;
if (!repoFullName || !prNumber) {
throw new Error('fetchPRConversationStep requires repoFullName and prNumber in input');
}

// Check if the project uses GitLab
const scmProvider = params.project?.id
? await getIntegrationProvider(params.project.id, 'scm')
: null;

if (scmProvider === 'gitlab') {
return fetchGitLabMRConversationStep(params, repoFullName, prNumber);
}

const injections: ContextInjection[] = [];
const { owner, repo } = parseRepoFullName(repoFullName);

Expand Down Expand Up @@ -272,6 +398,43 @@ export async function fetchPRConversationStep(
return injections;
}

async function fetchGitLabMRConversationStep(
params: FetchContextParams,
projectPath: string,
mrIid: number,
): Promise<ContextInjection[]> {
const injections: ContextInjection[] = [];

params.logWriter('INFO', 'Fetching MR conversation context from GitLab', {
projectPath,
mrIid,
});

const notes = await gitlabClient.getMRNotes(projectPath, mrIid);

// Filter to non-system notes (user comments only)
const userNotes = notes.filter((n) => !n.system);

const formatted = userNotes
.map(
(n) => `[${n.createdAt}] @${n.author.username}${n.resolved ? ' (resolved)' : ''}:\n${n.body}`,
)
.join('\n\n---\n\n');

injections.push({
toolName: 'GetMRNotes',
params: {
comment: 'Pre-fetching MR notes for conversation context',
projectPath,
mrIid,
},
result: formatted || '(No comments on this MR)',
description: 'Pre-fetched MR notes',
});

return injections;
}

export async function prepopulateTodosStep(
params: FetchContextParams,
): Promise<ContextInjection[]> {
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/resolve-conflicts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ triggers:
label: PR Conflict Detected
description: Trigger when a PR has merge conflicts with the base branch
defaultEnabled: false
providers: [github]
providers: [github, gitlab]
contextPipeline: [prContext, directoryListing, contextFiles, workItem]

strategies: {}
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ triggers:
label: Check Suite Failure
description: Trigger when CI checks fail
defaultEnabled: false
providers: [github]
providers: [github, gitlab]
contextPipeline: [prContext, directoryListing, contextFiles, workItem]

strategies: {}
Expand Down
2 changes: 1 addition & 1 deletion src/agents/definitions/respond-to-pr-comment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ triggers:
label: PR Comment @mention
description: Trigger when the implementer bot is @mentioned in a PR comment
defaultEnabled: false
providers: [github]
providers: [github, gitlab]
contextPipeline: [prContext, prConversation, directoryListing, contextFiles]

strategies:
Expand Down
4 changes: 2 additions & 2 deletions src/agents/definitions/respond-to-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ triggers:
label: PR Review Submitted
description: Trigger when a review with changes requested or comments is submitted
defaultEnabled: false
providers: [github]
providers: [github, gitlab]
contextPipeline: [prContext, prConversation, directoryListing, contextFiles]

strategies:
Expand All @@ -47,7 +47,7 @@ prompts:
<%= it.commentBody %>
---

Carefully read each review comment and make the requested changes. Commit and push your changes when done. Use the ReplyToReviewComment tool to respond to individual review comments as you address them. Focus on surgical, targeted fixes unless the reviewer clearly asks for broader changes.
Carefully read each review comment and make the requested changes. Commit and push your changes when done. Use the comment reply tool (ReplyToReviewComment or PostMRNote, whichever is available) to respond to individual review comments as you address them. Focus on surgical, targeted fixes unless the reviewer clearly asks for broader changes.

hooks:
trailing:
Expand Down
Loading
Loading