55 * These are the building blocks composed by the YAML contextPipeline arrays.
66 */
77
8+ import { getIntegrationProvider } from '../../db/repositories/credentialsRepository.js' ;
89import { formatCheckStatus } from '../../gadgets/github/core/getPRChecks.js' ;
910import { ListDirectory } from '../../gadgets/ListDirectory.js' ;
1011import {
@@ -20,7 +21,8 @@ import {
2021 initTodoSession ,
2122 saveTodos ,
2223} from '../../gadgets/todo/storage.js' ;
23- import { githubClient } from '../../github/client.js' ;
24+ import { githubClient , type PRDiffFile } from '../../github/client.js' ;
25+ import { gitlabClient } from '../../gitlab/client.js' ;
2426import { getJiraConfig , getLinearConfig , getTrelloConfig } from '../../pm/config.js' ;
2527import type {
2628 Attachment ,
@@ -153,6 +155,24 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
153155 if ( ! repoFullName || ! prNumber ) {
154156 throw new Error ( 'fetchPRContextStep requires repoFullName and prNumber in input' ) ;
155157 }
158+
159+ // Check if the project uses GitLab
160+ const scmProvider = params . project ?. id
161+ ? await getIntegrationProvider ( params . project . id , 'scm' )
162+ : null ;
163+
164+ if ( scmProvider === 'gitlab' ) {
165+ return fetchGitLabMRContextStep ( params , repoFullName , prNumber ) ;
166+ }
167+
168+ return fetchGitHubPRContextStep ( params , repoFullName , prNumber ) ;
169+ }
170+
171+ async function fetchGitHubPRContextStep (
172+ params : FetchContextParams ,
173+ repoFullName : string ,
174+ prNumber : number ,
175+ ) : Promise < ContextInjection [ ] > {
156176 const injections : ContextInjection [ ] = [ ] ;
157177 const { owner, repo } = parseRepoFullName ( repoFullName ) ;
158178
@@ -237,13 +257,114 @@ export async function fetchPRContextStep(params: FetchContextParams): Promise<Co
237257 return injections ;
238258}
239259
260+ async function fetchGitLabMRContextStep (
261+ params : FetchContextParams ,
262+ projectPath : string ,
263+ mrIid : number ,
264+ ) : Promise < ContextInjection [ ] > {
265+ const injections : ContextInjection [ ] = [ ] ;
266+
267+ params . logWriter ( 'INFO' , 'Fetching MR details and diff from GitLab' , {
268+ projectPath,
269+ mrIid,
270+ } ) ;
271+
272+ const mrDetails = await gitlabClient . getMR ( projectPath , mrIid ) ;
273+ const mrDiff = await gitlabClient . getMRDiff ( projectPath , mrIid ) ;
274+
275+ // Format MR details
276+ const detailsFormatted = [
277+ `MR #${ mrDetails . iid } : ${ mrDetails . title } ` ,
278+ `State: ${ mrDetails . state } ` ,
279+ `Author: ${ mrDetails . author . username } ` ,
280+ `Source: ${ mrDetails . sourceBranch } → Target: ${ mrDetails . targetBranch } ` ,
281+ `URL: ${ mrDetails . webUrl } ` ,
282+ mrDetails . description ? `\nDescription:\n${ mrDetails . description } ` : '' ,
283+ ]
284+ . filter ( Boolean )
285+ . join ( '\n' ) ;
286+
287+ injections . push ( {
288+ toolName : 'GetMRDetails' ,
289+ params : { comment : 'Pre-fetching MR details for review context' , projectPath, mrIid } ,
290+ result : detailsFormatted ,
291+ description : 'Pre-fetched MR details' ,
292+ } ) ;
293+
294+ // Compact per-file diffs sourced from the checked-out local MR workspace,
295+ // mirroring the GitHub PR context path. Files that don't fit the budget or
296+ // can't be diffed are surfaced in a separate SKIPPED FILES injection so the
297+ // agent can fetch them on demand.
298+ const prDiffCompat : PRDiffFile [ ] = mrDiff . map ( ( f ) => ( {
299+ filename : f . newPath ,
300+ previousFilename : f . renamedFile ? f . oldPath : undefined ,
301+ status : f . newFile
302+ ? 'added'
303+ : f . deletedFile
304+ ? 'removed'
305+ : f . renamedFile
306+ ? 'renamed'
307+ : 'modified' ,
308+ additions : 0 ,
309+ deletions : 0 ,
310+ changes : 0 ,
311+ patch : f . diff ,
312+ } ) ) ;
313+ const localDiffSource = await sourceLocalPRDiffs ( {
314+ files : prDiffCompat ,
315+ repoDir : params . repoDir ,
316+ baseBranch : mrDetails . targetBranch ,
317+ logWriter : params . logWriter ,
318+ } ) ;
319+ const diffContext = extractPRDiffs ( localDiffSource . files ) ;
320+ const skipReasons = countSkipsByReason ( diffContext . skipped ) ;
321+ params . logWriter ( 'INFO' , 'MR context prepared' , {
322+ included : diffContext . included . length ,
323+ skipped : diffContext . skipped . length ,
324+ skipReasons,
325+ totalDiffTokens : diffContext . totalDiffTokens ,
326+ perFileTokenCap : diffContext . perFileTokenCap ,
327+ } ) ;
328+
329+ injections . push ( {
330+ toolName : 'GetMRDiff' ,
331+ params : { comment : 'Pre-fetching compact per-file diffs for review' , projectPath, mrIid } ,
332+ result : formatPRDiffContext ( diffContext ) ,
333+ description : 'Pre-fetched MR diff context' ,
334+ } ) ;
335+
336+ if ( diffContext . skipped . length > 0 ) {
337+ injections . push ( {
338+ toolName : 'SkippedFiles' ,
339+ params : {
340+ comment : 'MR files omitted from the compact context — fetch on demand if relevant' ,
341+ prNumber : mrIid ,
342+ } ,
343+ result : formatSkippedFilesInjection ( diffContext . skipped , mrIid ) ,
344+ description : 'Skipped files' ,
345+ } ) ;
346+ }
347+
348+ return injections ;
349+ }
350+
240351export async function fetchPRConversationStep (
241352 params : FetchContextParams ,
242353) : Promise < ContextInjection [ ] > {
243354 const { repoFullName, prNumber } = params . input ;
244355 if ( ! repoFullName || ! prNumber ) {
245356 throw new Error ( 'fetchPRConversationStep requires repoFullName and prNumber in input' ) ;
246357 }
358+
359+ // Check if the project uses GitLab
360+ const scmProvider = params . project ?. id
361+ ? await getIntegrationProvider ( params . project . id , 'scm' )
362+ : null ;
363+
364+ if ( scmProvider === 'gitlab' ) {
365+ return fetchGitLabMRConversationStep ( params , repoFullName , prNumber ) ;
366+ }
367+
247368 const injections : ContextInjection [ ] = [ ] ;
248369 const { owner, repo } = parseRepoFullName ( repoFullName ) ;
249370
@@ -294,6 +415,43 @@ export async function fetchPRConversationStep(
294415 return injections ;
295416}
296417
418+ async function fetchGitLabMRConversationStep (
419+ params : FetchContextParams ,
420+ projectPath : string ,
421+ mrIid : number ,
422+ ) : Promise < ContextInjection [ ] > {
423+ const injections : ContextInjection [ ] = [ ] ;
424+
425+ params . logWriter ( 'INFO' , 'Fetching MR conversation context from GitLab' , {
426+ projectPath,
427+ mrIid,
428+ } ) ;
429+
430+ const notes = await gitlabClient . getMRNotes ( projectPath , mrIid ) ;
431+
432+ // Filter to non-system notes (user comments only)
433+ const userNotes = notes . filter ( ( n ) => ! n . system ) ;
434+
435+ const formatted = userNotes
436+ . map (
437+ ( n ) => `[${ n . createdAt } ] @${ n . author . username } ${ n . resolved ? ' (resolved)' : '' } :\n${ n . body } ` ,
438+ )
439+ . join ( '\n\n---\n\n' ) ;
440+
441+ injections . push ( {
442+ toolName : 'GetMRNotes' ,
443+ params : {
444+ comment : 'Pre-fetching MR notes for conversation context' ,
445+ projectPath,
446+ mrIid,
447+ } ,
448+ result : formatted || '(No comments on this MR)' ,
449+ description : 'Pre-fetched MR notes' ,
450+ } ) ;
451+
452+ return injections ;
453+ }
454+
297455export async function prepopulateTodosStep (
298456 params : FetchContextParams ,
299457) : Promise < ContextInjection [ ] > {
0 commit comments