cyrus-edge-worker 0.2.37 → 0.2.39
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/dist/AgentSessionManager.d.ts +2 -2
- package/dist/AgentSessionManager.d.ts.map +1 -1
- package/dist/AgentSessionManager.js +1 -1
- package/dist/AgentSessionManager.js.map +1 -1
- package/dist/AttachmentService.d.ts +4 -0
- package/dist/AttachmentService.d.ts.map +1 -1
- package/dist/AttachmentService.js +6 -0
- package/dist/AttachmentService.js.map +1 -1
- package/dist/EdgeWorker.d.ts +44 -0
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +498 -47
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/McpConfigService.d.ts.map +1 -1
- package/dist/McpConfigService.js +4 -0
- package/dist/McpConfigService.js.map +1 -1
- package/dist/PromptBuilder.d.ts.map +1 -1
- package/dist/PromptBuilder.js +6 -1
- package/dist/PromptBuilder.js.map +1 -1
- package/dist/RepositoryRouter.d.ts.map +1 -1
- package/dist/RepositoryRouter.js +8 -5
- package/dist/RepositoryRouter.js.map +1 -1
- package/dist/SlackChatAdapter.d.ts.map +1 -1
- package/dist/SlackChatAdapter.js +4 -0
- package/dist/SlackChatAdapter.js.map +1 -1
- package/dist/ToolPermissionResolver.js +1 -1
- package/dist/ToolPermissionResolver.js.map +1 -1
- package/dist/procedures/registry.d.ts +22 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +34 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/prompts/subroutines/changelog-update-gitlab.md +79 -0
- package/dist/prompts/subroutines/glab-mr.md +106 -0
- package/package.json +15 -14
package/dist/EdgeWorker.js
CHANGED
|
@@ -10,6 +10,7 @@ import { CLIIssueTrackerService, CLIRPCServer, createLogger, DEFAULT_PROXY_URL,
|
|
|
10
10
|
import { CursorRunner } from "cyrus-cursor-runner";
|
|
11
11
|
import { GeminiRunner } from "cyrus-gemini-runner";
|
|
12
12
|
import { extractCommentAuthor, extractCommentBody, extractCommentId, extractCommentUrl, extractPRBaseBranchRef, extractPRBranchRef, extractPRNumber, extractPRTitle, extractRepoFullName, extractRepoName, extractRepoOwner, extractSessionKey, GitHubCommentService, GitHubEventTransport, isCommentOnPullRequest, isIssueCommentPayload, isPullRequestReviewCommentPayload, isPullRequestReviewPayload, stripMention, } from "cyrus-github-event-transport";
|
|
13
|
+
import { extractDiscussionId, extractSessionKey as extractGitLabSessionKey, extractMRBaseBranchRef, extractMRBranchRef, extractMRIid, extractMRTitle, extractNoteAuthor, extractNoteBody, extractNoteId, extractNoteUrl, extractProjectId, extractProjectPath, GitLabCommentService, GitLabEventTransport, isNoteOnMergeRequest, stripMention as stripGitLabMention, } from "cyrus-gitlab-event-transport";
|
|
13
14
|
import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
|
|
14
15
|
import { createCyrusToolsServer, } from "cyrus-mcp-tools";
|
|
15
16
|
import { SlackEventTransport, } from "cyrus-slack-event-transport";
|
|
@@ -24,7 +25,7 @@ import { GitService } from "./GitService.js";
|
|
|
24
25
|
import { GlobalSessionRegistry } from "./GlobalSessionRegistry.js";
|
|
25
26
|
import { McpConfigService } from "./McpConfigService.js";
|
|
26
27
|
import { PromptBuilder } from "./PromptBuilder.js";
|
|
27
|
-
import { ProcedureAnalyzer, } from "./procedures/index.js";
|
|
28
|
+
import { applyPlatformSubroutines, ProcedureAnalyzer, } from "./procedures/index.js";
|
|
28
29
|
import { RepositoryRouter, } from "./RepositoryRouter.js";
|
|
29
30
|
import { RunnerConfigBuilder } from "./RunnerConfigBuilder.js";
|
|
30
31
|
import { RunnerSelectionService } from "./RunnerSelectionService.js";
|
|
@@ -48,9 +49,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
48
49
|
issueTrackers = new Map(); // one issue tracker per Linear workspace (keyed by linearWorkspaceId)
|
|
49
50
|
linearEventTransport = null; // Single event transport for webhook delivery
|
|
50
51
|
gitHubEventTransport = null; // GitHub event transport for forwarded GitHub webhooks
|
|
52
|
+
gitLabEventTransport = null; // GitLab event transport for forwarded GitLab webhooks
|
|
51
53
|
slackEventTransport = null;
|
|
52
54
|
chatSessionHandler = null;
|
|
53
55
|
gitHubCommentService; // Service for posting comments back to GitHub PRs
|
|
56
|
+
gitLabCommentService; // Service for posting comments back to GitLab MRs
|
|
54
57
|
cliRPCServer = null; // CLI RPC server for CLI platform mode
|
|
55
58
|
configUpdater = null; // Single config updater for configuration updates
|
|
56
59
|
persistenceManager;
|
|
@@ -95,6 +98,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
95
98
|
this.persistenceManager = new PersistenceManager(join(this.cyrusHome, "state"));
|
|
96
99
|
// Initialize GitHub comment service for posting replies to GitHub PRs
|
|
97
100
|
this.gitHubCommentService = new GitHubCommentService();
|
|
101
|
+
// Initialize GitLab comment service for posting replies to GitLab MRs
|
|
102
|
+
this.gitLabCommentService = new GitLabCommentService();
|
|
98
103
|
// Initialize global session registry (centralized session storage)
|
|
99
104
|
this.globalSessionRegistry = new GlobalSessionRegistry();
|
|
100
105
|
// Initialize procedure router for fast classification
|
|
@@ -288,6 +293,8 @@ export class EdgeWorker extends EventEmitter {
|
|
|
288
293
|
await this.removeDeletedRepositories(changes.removed);
|
|
289
294
|
await this.updateModifiedRepositories(changes.modified);
|
|
290
295
|
await this.addNewRepositories(changes.added);
|
|
296
|
+
// Detect and apply workspace token changes before overwriting config
|
|
297
|
+
this.updateLinearWorkspaceTokens(changes.newConfig);
|
|
291
298
|
const prevDefaultRunner = this.config.defaultRunner;
|
|
292
299
|
this.config = changes.newConfig;
|
|
293
300
|
this.configManager.setConfig(changes.newConfig);
|
|
@@ -321,50 +328,39 @@ export class EdgeWorker extends EventEmitter {
|
|
|
321
328
|
* Initialize and register components (routes) before server starts
|
|
322
329
|
*/
|
|
323
330
|
async initializeComponents() {
|
|
324
|
-
//
|
|
325
|
-
const firstRepo = Array.from(this.repositories.values())[0];
|
|
326
|
-
if (!firstRepo) {
|
|
327
|
-
throw new Error("No active repositories configured");
|
|
328
|
-
}
|
|
329
|
-
// Platform-specific initialization
|
|
331
|
+
// 1. Platform-specific initialization
|
|
330
332
|
if (this.config.platform === "cli") {
|
|
331
|
-
// CLI mode:
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
// CLI mode: find any available CLIIssueTrackerService
|
|
334
|
+
const firstCliTracker = Array.from(this.issueTrackers.values()).find((tracker) => tracker instanceof CLIIssueTrackerService);
|
|
335
|
+
if (firstCliTracker) {
|
|
336
|
+
this.cliRPCServer = new CLIRPCServer({
|
|
337
|
+
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
338
|
+
issueTracker: firstCliTracker,
|
|
339
|
+
version: "1.0.0",
|
|
340
|
+
});
|
|
341
|
+
// Register the /cli/rpc endpoint
|
|
342
|
+
this.cliRPCServer.register();
|
|
343
|
+
this.logger.info("✅ CLI RPC server registered");
|
|
344
|
+
this.logger.info(" RPC endpoint: /cli/rpc");
|
|
345
|
+
// Create CLI event transport and register listener
|
|
346
|
+
const cliEventTransport = firstCliTracker.createEventTransport({
|
|
347
|
+
platform: "cli",
|
|
348
|
+
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
349
|
+
});
|
|
350
|
+
// Listen for webhook events
|
|
351
|
+
cliEventTransport.on("event", (event) => {
|
|
352
|
+
const repos = Array.from(this.repositories.values());
|
|
353
|
+
this.handleWebhook(event, repos);
|
|
354
|
+
});
|
|
355
|
+
// Listen for errors
|
|
356
|
+
cliEventTransport.on("error", (error) => {
|
|
357
|
+
this.handleError(error);
|
|
358
|
+
});
|
|
359
|
+
// Register the CLI event transport endpoints
|
|
360
|
+
cliEventTransport.register();
|
|
361
|
+
this.logger.info("✅ CLI event transport registered");
|
|
362
|
+
this.logger.info(" Event listener: listening for AgentSessionCreated events");
|
|
339
363
|
}
|
|
340
|
-
this.cliRPCServer = new CLIRPCServer({
|
|
341
|
-
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
342
|
-
issueTracker: firstIssueTracker,
|
|
343
|
-
version: "1.0.0",
|
|
344
|
-
});
|
|
345
|
-
// Register the /cli/rpc endpoint
|
|
346
|
-
this.cliRPCServer.register();
|
|
347
|
-
this.logger.info("✅ CLI RPC server registered");
|
|
348
|
-
this.logger.info(" RPC endpoint: /cli/rpc");
|
|
349
|
-
// Create CLI event transport and register listener
|
|
350
|
-
const cliEventTransport = firstIssueTracker.createEventTransport({
|
|
351
|
-
platform: "cli",
|
|
352
|
-
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
353
|
-
});
|
|
354
|
-
// Listen for webhook events (same pattern as Linear mode)
|
|
355
|
-
cliEventTransport.on("event", (event) => {
|
|
356
|
-
// Get all active repositories for webhook handling
|
|
357
|
-
const repos = Array.from(this.repositories.values());
|
|
358
|
-
this.handleWebhook(event, repos);
|
|
359
|
-
});
|
|
360
|
-
// Listen for errors
|
|
361
|
-
cliEventTransport.on("error", (error) => {
|
|
362
|
-
this.handleError(error);
|
|
363
|
-
});
|
|
364
|
-
// Register the CLI event transport endpoints
|
|
365
|
-
cliEventTransport.register();
|
|
366
|
-
this.logger.info("✅ CLI event transport registered");
|
|
367
|
-
this.logger.info(" Event listener: listening for AgentSessionCreated events");
|
|
368
364
|
}
|
|
369
365
|
else {
|
|
370
366
|
// Linear mode: Create and register LinearEventTransport
|
|
@@ -381,7 +377,6 @@ export class EdgeWorker extends EventEmitter {
|
|
|
381
377
|
});
|
|
382
378
|
// Listen for legacy webhook events (deprecated, kept for backward compatibility)
|
|
383
379
|
this.linearEventTransport.on("event", (event) => {
|
|
384
|
-
// Get all active repositories for webhook handling
|
|
385
380
|
const repos = Array.from(this.repositories.values());
|
|
386
381
|
this.handleWebhook(event, repos);
|
|
387
382
|
});
|
|
@@ -398,10 +393,11 @@ export class EdgeWorker extends EventEmitter {
|
|
|
398
393
|
this.logger.info(`✅ Linear event transport registered (${verificationMode} mode)`);
|
|
399
394
|
this.logger.info(` Webhook endpoint: ${this.sharedApplicationServer.getWebhookUrl()}`);
|
|
400
395
|
}
|
|
401
|
-
// 2. Register GitHub
|
|
402
|
-
//
|
|
396
|
+
// 2. Register GitHub and Slack event transports unconditionally
|
|
397
|
+
// These don't require repositories and must be available during onboarding
|
|
398
|
+
// for webhook URL verification to succeed.
|
|
403
399
|
this.registerGitHubEventTransport();
|
|
404
|
-
|
|
400
|
+
this.registerGitLabEventTransport();
|
|
405
401
|
this.registerSlackEventTransport();
|
|
406
402
|
// 3. Create and register ConfigUpdater (both platforms)
|
|
407
403
|
this.configUpdater = new ConfigUpdater(this.sharedApplicationServer.getFastifyInstance(), this.cyrusHome, process.env.CYRUS_API_KEY || "");
|
|
@@ -486,6 +482,43 @@ export class EdgeWorker extends EventEmitter {
|
|
|
486
482
|
this.logger.info(`GitHub event transport registered (${verificationMode} mode)`);
|
|
487
483
|
this.logger.info("Webhook endpoint: POST /github-webhook");
|
|
488
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Register the GitLab event transport for receiving forwarded GitLab webhooks.
|
|
487
|
+
* This creates a /gitlab-webhook endpoint that handles note events on merge requests.
|
|
488
|
+
*/
|
|
489
|
+
registerGitLabEventTransport() {
|
|
490
|
+
const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
|
|
491
|
+
const hasGitlabWebhookSecret = process.env.GITLAB_WEBHOOK_SECRET != null &&
|
|
492
|
+
process.env.GITLAB_WEBHOOK_SECRET !== "";
|
|
493
|
+
const useSignatureVerification = isExternalHost && hasGitlabWebhookSecret;
|
|
494
|
+
const verificationMode = useSignatureVerification ? "signature" : "proxy";
|
|
495
|
+
const secret = useSignatureVerification
|
|
496
|
+
? process.env.GITLAB_WEBHOOK_SECRET
|
|
497
|
+
: process.env.CYRUS_API_KEY || "";
|
|
498
|
+
this.gitLabEventTransport = new GitLabEventTransport({
|
|
499
|
+
fastifyServer: this.sharedApplicationServer.getFastifyInstance(),
|
|
500
|
+
verificationMode,
|
|
501
|
+
secret,
|
|
502
|
+
});
|
|
503
|
+
// Listen for legacy GitLab webhook events
|
|
504
|
+
this.gitLabEventTransport.on("event", (event) => {
|
|
505
|
+
this.handleGitLabWebhook(event).catch((error) => {
|
|
506
|
+
this.logger.error("Failed to handle GitLab webhook", error instanceof Error ? error : new Error(String(error)));
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
// Listen for unified internal messages (new message bus)
|
|
510
|
+
this.gitLabEventTransport.on("message", (message) => {
|
|
511
|
+
this.handleMessage(message);
|
|
512
|
+
});
|
|
513
|
+
// Listen for errors
|
|
514
|
+
this.gitLabEventTransport.on("error", (error) => {
|
|
515
|
+
this.handleError(error);
|
|
516
|
+
});
|
|
517
|
+
// Register the /gitlab-webhook endpoint
|
|
518
|
+
this.gitLabEventTransport.register();
|
|
519
|
+
this.logger.info(`GitLab event transport registered (${verificationMode} mode)`);
|
|
520
|
+
this.logger.info("Webhook endpoint: POST /gitlab-webhook");
|
|
521
|
+
}
|
|
489
522
|
/**
|
|
490
523
|
* Register the Slack event transport for receiving forwarded Slack webhooks from CYHOST.
|
|
491
524
|
* This creates a /slack-webhook endpoint that handles @mention events from Slack.
|
|
@@ -1009,6 +1042,373 @@ ${taskSection}`;
|
|
|
1009
1042
|
this.logger.error("Failed to post GitHub reply", error instanceof Error ? error : new Error(String(error)));
|
|
1010
1043
|
}
|
|
1011
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Handle an incoming GitLab webhook event (note on a merge request).
|
|
1047
|
+
* Mirrors the GitHub webhook handler but uses GitLab-specific utilities.
|
|
1048
|
+
*/
|
|
1049
|
+
async handleGitLabWebhook(event) {
|
|
1050
|
+
this.activeWebhookCount++;
|
|
1051
|
+
try {
|
|
1052
|
+
// Only handle notes on merge requests
|
|
1053
|
+
if (!isNoteOnMergeRequest(event)) {
|
|
1054
|
+
this.logger.debug("Ignoring GitLab event: not a note on a merge request");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const projectPath = extractProjectPath(event);
|
|
1058
|
+
const mrIid = extractMRIid(event);
|
|
1059
|
+
const noteBody = extractNoteBody(event);
|
|
1060
|
+
const noteAuthor = extractNoteAuthor(event);
|
|
1061
|
+
const mrTitle = extractMRTitle(event);
|
|
1062
|
+
const sessionKey = extractGitLabSessionKey(event);
|
|
1063
|
+
// Skip comments from the bot itself to prevent infinite loops
|
|
1064
|
+
const botUsername = process.env.GITLAB_BOT_USERNAME;
|
|
1065
|
+
if (botUsername && noteAuthor === botUsername) {
|
|
1066
|
+
this.logger.debug(`Ignoring note from bot user @${botUsername} on ${projectPath}!${mrIid}`);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
// Only trigger on notes that mention the bot (when configured)
|
|
1070
|
+
if (botUsername && !noteBody.includes(`@${botUsername}`)) {
|
|
1071
|
+
this.logger.debug(`Ignoring note without @${botUsername} mention on ${projectPath}!${mrIid}`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.logger.info(`Processing GitLab webhook: ${projectPath}!${mrIid} by @${noteAuthor}`);
|
|
1075
|
+
// Add "eyes" emoji reaction to acknowledge receipt
|
|
1076
|
+
const reactionToken = event.accessToken || process.env.GITLAB_ACCESS_TOKEN;
|
|
1077
|
+
const noteId = extractNoteId(event);
|
|
1078
|
+
const projectId = extractProjectId(event);
|
|
1079
|
+
if (reactionToken && noteId && projectId && mrIid) {
|
|
1080
|
+
this.gitLabCommentService
|
|
1081
|
+
.addAwardEmoji({
|
|
1082
|
+
token: reactionToken,
|
|
1083
|
+
projectId,
|
|
1084
|
+
mrIid,
|
|
1085
|
+
noteId,
|
|
1086
|
+
name: "eyes",
|
|
1087
|
+
})
|
|
1088
|
+
.catch((err) => {
|
|
1089
|
+
this.logger.warn(`Failed to add GitLab emoji reaction: ${err instanceof Error ? err.message : err}`);
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
// Find the repository configuration that matches this GitLab project
|
|
1093
|
+
const repository = this.findRepositoryByGitLabUrl(projectPath);
|
|
1094
|
+
if (!repository) {
|
|
1095
|
+
this.logger.warn(`No repository configured for GitLab project: ${projectPath}`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const agentSessionManager = this.agentSessionManager;
|
|
1099
|
+
// Branch refs are available directly from the MR payload
|
|
1100
|
+
const branchRef = extractMRBranchRef(event);
|
|
1101
|
+
const baseBranchRef = extractMRBaseBranchRef(event);
|
|
1102
|
+
if (!branchRef || !mrIid) {
|
|
1103
|
+
this.logger.error(`Could not determine branch or MR iid for ${projectPath}!${mrIid}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
// Strip the bot mention to get the task instructions
|
|
1107
|
+
const mentionHandle = botUsername ? `@${botUsername}` : "@cyrusagent";
|
|
1108
|
+
const taskInstructions = stripGitLabMention(noteBody, mentionHandle);
|
|
1109
|
+
// Check for an existing multi-repo session that includes this repository
|
|
1110
|
+
let workspace = null;
|
|
1111
|
+
const multiRepoSession = agentSessionManager.getActiveMultiRepoSessionForRepository(repository.id);
|
|
1112
|
+
if (multiRepoSession) {
|
|
1113
|
+
const subWorktreePath = multiRepoSession.workspace.repoPaths?.[repository.id];
|
|
1114
|
+
if (subWorktreePath) {
|
|
1115
|
+
workspace = {
|
|
1116
|
+
path: subWorktreePath,
|
|
1117
|
+
isGitWorktree: true,
|
|
1118
|
+
};
|
|
1119
|
+
this.logger.info(`Resolved multi-repo sub-worktree for ${repository.name}: ${subWorktreePath}`);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
this.logger.warn(`No sub-worktree found for repo ${repository.name} in multi-repo session ${multiRepoSession.id}, falling back to root workspace`);
|
|
1123
|
+
workspace = {
|
|
1124
|
+
path: multiRepoSession.workspace.path,
|
|
1125
|
+
isGitWorktree: true,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
// Single-repo or no existing session: create workspace
|
|
1131
|
+
workspace = await this.createGitLabWorkspace(repository, branchRef, mrIid);
|
|
1132
|
+
}
|
|
1133
|
+
if (!workspace) {
|
|
1134
|
+
this.logger.error(`Failed to create workspace for ${projectPath}!${mrIid}`);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
this.logger.info(`GitLab workspace created at: ${workspace.path}`);
|
|
1138
|
+
// Check if another active session is already using this branch/workspace
|
|
1139
|
+
const existingSessions = agentSessionManager.getActiveSessionsByBranchName(branchRef);
|
|
1140
|
+
const firstExisting = existingSessions[0];
|
|
1141
|
+
if (firstExisting) {
|
|
1142
|
+
this.logger.warn(`Reusing workspace from active session ${firstExisting.id} — concurrent writes possible`);
|
|
1143
|
+
}
|
|
1144
|
+
// Create a synthetic session for this GitLab MR note
|
|
1145
|
+
const issueMinimal = {
|
|
1146
|
+
id: sessionKey,
|
|
1147
|
+
identifier: `${projectPath}!${mrIid}`,
|
|
1148
|
+
title: mrTitle || `MR !${mrIid}`,
|
|
1149
|
+
branchName: branchRef,
|
|
1150
|
+
};
|
|
1151
|
+
// Create an internal agent session (no Linear session for GitLab)
|
|
1152
|
+
const gitlabSessionId = `gitlab-${Date.now()}`;
|
|
1153
|
+
agentSessionManager.createCyrusAgentSession(gitlabSessionId, sessionKey, issueMinimal, workspace, "gitlab", // Don't stream activities to Linear for GitLab sources
|
|
1154
|
+
[
|
|
1155
|
+
{
|
|
1156
|
+
repositoryId: repository.id,
|
|
1157
|
+
branchName: branchRef,
|
|
1158
|
+
baseBranchName: baseBranchRef ?? repository.baseBranch,
|
|
1159
|
+
},
|
|
1160
|
+
]);
|
|
1161
|
+
// Register session-to-repo mapping and activity sink
|
|
1162
|
+
this.sessionRepositories.set(gitlabSessionId, repository.id);
|
|
1163
|
+
const activitySink = this.activitySinks.get(repository.id);
|
|
1164
|
+
if (activitySink) {
|
|
1165
|
+
agentSessionManager.setActivitySink(gitlabSessionId, activitySink);
|
|
1166
|
+
}
|
|
1167
|
+
const session = agentSessionManager.getSession(gitlabSessionId);
|
|
1168
|
+
if (!session) {
|
|
1169
|
+
this.logger.error(`Failed to create session for GitLab webhook on ${projectPath}!${mrIid}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// Initialize procedure metadata
|
|
1173
|
+
if (!session.metadata) {
|
|
1174
|
+
session.metadata = {};
|
|
1175
|
+
}
|
|
1176
|
+
// Store GitLab-specific metadata for reply posting
|
|
1177
|
+
// Reuse commentId for note ID (serves the same purpose across platforms)
|
|
1178
|
+
session.metadata.commentId = String(noteId);
|
|
1179
|
+
// Build the system prompt for this GitLab MR session
|
|
1180
|
+
// TODO: Use buildGitLabChangeRequestSystemPrompt for merge_request approval events
|
|
1181
|
+
const isMergeRequestEvent = event.eventType === "merge_request";
|
|
1182
|
+
const systemPrompt = isMergeRequestEvent
|
|
1183
|
+
? this.buildGitLabChangeRequestSystemPrompt(event, branchRef, taskInstructions)
|
|
1184
|
+
: this.buildGitLabSystemPrompt(event, branchRef, taskInstructions);
|
|
1185
|
+
// Build allowed tools and directories
|
|
1186
|
+
// Exclude Slack MCP tools from GitLab sessions
|
|
1187
|
+
const allowedTools = this.buildAllowedTools(repository).filter((t) => t !== "mcp__slack");
|
|
1188
|
+
const disallowedTools = this.buildDisallowedTools(repository);
|
|
1189
|
+
const allowedDirectories = [repository.repositoryPath];
|
|
1190
|
+
// Create agent runner using the standard config builder
|
|
1191
|
+
const { config: runnerConfig, runnerType } = this.buildAgentRunnerConfig(session, repository, gitlabSessionId, systemPrompt, allowedTools, allowedDirectories, disallowedTools, undefined, // resumeSessionId
|
|
1192
|
+
undefined, // labels
|
|
1193
|
+
undefined, // issueDescription
|
|
1194
|
+
200, // maxTurns
|
|
1195
|
+
false, // singleTurn
|
|
1196
|
+
undefined, // disallowAllTools
|
|
1197
|
+
{ excludeSlackMcp: true });
|
|
1198
|
+
const runner = this.createRunnerForType(runnerType, runnerConfig);
|
|
1199
|
+
// Store the runner in the session manager
|
|
1200
|
+
agentSessionManager.addAgentRunner(gitlabSessionId, runner);
|
|
1201
|
+
// Save persisted state
|
|
1202
|
+
await this.savePersistedState();
|
|
1203
|
+
this.emit("session:started", sessionKey, issueMinimal, repository.id);
|
|
1204
|
+
this.logger.info(`Starting ${runnerType} runner for GitLab MR ${projectPath}!${mrIid}`);
|
|
1205
|
+
// Start the session and handle completion
|
|
1206
|
+
try {
|
|
1207
|
+
const sessionInfo = await runner.start(taskInstructions);
|
|
1208
|
+
this.logger.info(`GitLab session started: ${sessionInfo.sessionId}`);
|
|
1209
|
+
// When session completes, post the reply back to GitLab
|
|
1210
|
+
await this.postGitLabReply(event, runner, repository);
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
this.logger.error(`GitLab session error for ${projectPath}!${mrIid}`, error instanceof Error ? error : new Error(String(error)));
|
|
1214
|
+
}
|
|
1215
|
+
finally {
|
|
1216
|
+
await this.savePersistedState();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
this.logger.error("Failed to process GitLab webhook", error instanceof Error ? error : new Error(String(error)));
|
|
1221
|
+
}
|
|
1222
|
+
finally {
|
|
1223
|
+
this.activeWebhookCount--;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Find a repository configuration that matches a GitLab project URL.
|
|
1228
|
+
* Matches against the gitlabUrl field in repository config.
|
|
1229
|
+
*/
|
|
1230
|
+
findRepositoryByGitLabUrl(projectPath) {
|
|
1231
|
+
for (const repo of this.repositories.values()) {
|
|
1232
|
+
if (!repo.gitlabUrl)
|
|
1233
|
+
continue;
|
|
1234
|
+
if (repo.gitlabUrl.includes(projectPath) ||
|
|
1235
|
+
repo.gitlabUrl.endsWith(`/${projectPath}`)) {
|
|
1236
|
+
return repo;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Create a git worktree for a GitLab MR branch.
|
|
1243
|
+
* If the worktree already exists for this branch, reuse it.
|
|
1244
|
+
*/
|
|
1245
|
+
async createGitLabWorkspace(repository, branchRef, mrIid) {
|
|
1246
|
+
try {
|
|
1247
|
+
// Create a synthetic issue-like object for the git service
|
|
1248
|
+
const syntheticIssue = {
|
|
1249
|
+
id: `gitlab-mr-${mrIid}`,
|
|
1250
|
+
identifier: `MR-${mrIid}`,
|
|
1251
|
+
title: `MR !${mrIid}`,
|
|
1252
|
+
description: null,
|
|
1253
|
+
url: "",
|
|
1254
|
+
branchName: branchRef,
|
|
1255
|
+
assigneeId: null,
|
|
1256
|
+
stateId: null,
|
|
1257
|
+
teamId: null,
|
|
1258
|
+
labelIds: [],
|
|
1259
|
+
priority: 0,
|
|
1260
|
+
createdAt: new Date(),
|
|
1261
|
+
updatedAt: new Date(),
|
|
1262
|
+
archivedAt: null,
|
|
1263
|
+
state: Promise.resolve(undefined),
|
|
1264
|
+
assignee: Promise.resolve(undefined),
|
|
1265
|
+
team: Promise.resolve(undefined),
|
|
1266
|
+
parent: Promise.resolve(undefined),
|
|
1267
|
+
project: Promise.resolve(undefined),
|
|
1268
|
+
labels: () => Promise.resolve({ nodes: [] }),
|
|
1269
|
+
comments: () => Promise.resolve({ nodes: [] }),
|
|
1270
|
+
attachments: () => Promise.resolve({ nodes: [] }),
|
|
1271
|
+
children: () => Promise.resolve({ nodes: [] }),
|
|
1272
|
+
inverseRelations: () => Promise.resolve({ nodes: [] }),
|
|
1273
|
+
update: () => Promise.resolve({
|
|
1274
|
+
success: true,
|
|
1275
|
+
issue: undefined,
|
|
1276
|
+
lastSyncId: 0,
|
|
1277
|
+
}),
|
|
1278
|
+
};
|
|
1279
|
+
return await this.gitService.createGitWorktree(syntheticIssue, [
|
|
1280
|
+
repository,
|
|
1281
|
+
]);
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
this.logger.error(`Failed to create GitLab workspace for MR !${mrIid}`, error instanceof Error ? error : new Error(String(error)));
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Build a system prompt for a GitLab MR note session.
|
|
1290
|
+
*/
|
|
1291
|
+
buildGitLabSystemPrompt(event, branchRef, taskInstructions) {
|
|
1292
|
+
const projectPath = extractProjectPath(event);
|
|
1293
|
+
const mrIid = extractMRIid(event);
|
|
1294
|
+
const mrTitle = extractMRTitle(event);
|
|
1295
|
+
const noteAuthor = extractNoteAuthor(event);
|
|
1296
|
+
const noteUrl = extractNoteUrl(event);
|
|
1297
|
+
return `You are working on a GitLab Merge Request.
|
|
1298
|
+
|
|
1299
|
+
## Context
|
|
1300
|
+
- **Project**: ${projectPath}
|
|
1301
|
+
- **MR**: !${mrIid} - ${mrTitle || "Untitled"}
|
|
1302
|
+
- **Branch**: ${branchRef}
|
|
1303
|
+
- **Requested by**: @${noteAuthor}
|
|
1304
|
+
- **Note URL**: ${noteUrl}
|
|
1305
|
+
|
|
1306
|
+
## Task
|
|
1307
|
+
${taskInstructions}
|
|
1308
|
+
|
|
1309
|
+
## Instructions
|
|
1310
|
+
- You are already checked out on the MR branch \`${branchRef}\`
|
|
1311
|
+
- Make changes directly to the code on this branch
|
|
1312
|
+
- After making changes, commit and push them to the branch
|
|
1313
|
+
- Use \`glab\` CLI commands for GitLab-specific operations
|
|
1314
|
+
- Be concise in your responses as they will be posted back to the GitLab MR`;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Build a system prompt for a GitLab MR change request session.
|
|
1318
|
+
*/
|
|
1319
|
+
buildGitLabChangeRequestSystemPrompt(event, branchRef, reviewBody) {
|
|
1320
|
+
const projectPath = extractProjectPath(event);
|
|
1321
|
+
const mrIid = extractMRIid(event);
|
|
1322
|
+
const mrTitle = extractMRTitle(event);
|
|
1323
|
+
const noteAuthor = extractNoteAuthor(event);
|
|
1324
|
+
const noteUrl = extractNoteUrl(event);
|
|
1325
|
+
const hasReviewBody = reviewBody.trim().length > 0;
|
|
1326
|
+
const taskSection = hasReviewBody
|
|
1327
|
+
? `## Reviewer Feedback
|
|
1328
|
+
${reviewBody}
|
|
1329
|
+
|
|
1330
|
+
## Instructions
|
|
1331
|
+
- Read the MR diff and the reviewer's feedback above to understand all requested changes
|
|
1332
|
+
- You are already checked out on the MR branch \`${branchRef}\`
|
|
1333
|
+
- Address all the reviewer's feedback and make the necessary changes
|
|
1334
|
+
- After making changes, commit and push them to the branch
|
|
1335
|
+
- Respond with a concise summary of the changes you made`
|
|
1336
|
+
: `## Instructions
|
|
1337
|
+
- The reviewer has requested changes but did not leave a summary comment
|
|
1338
|
+
- Use \`glab mr view ${mrIid}\` and \`glab mr diff ${mrIid}\` to review the MR context
|
|
1339
|
+
- You are already checked out on the MR branch \`${branchRef}\`
|
|
1340
|
+
- Address all the reviewer's feedback and make the necessary changes
|
|
1341
|
+
- After making changes, commit and push them to the branch
|
|
1342
|
+
- Respond with a concise summary of the changes you made`;
|
|
1343
|
+
return `You are working on a GitLab Merge Request that has received a change request review.
|
|
1344
|
+
|
|
1345
|
+
## Context
|
|
1346
|
+
- **Project**: ${projectPath}
|
|
1347
|
+
- **MR**: !${mrIid} - ${mrTitle || "Untitled"}
|
|
1348
|
+
- **Branch**: ${branchRef}
|
|
1349
|
+
- **Reviewer**: @${noteAuthor}
|
|
1350
|
+
- **Note URL**: ${noteUrl}
|
|
1351
|
+
|
|
1352
|
+
${taskSection}`;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Post a reply back to the GitLab MR after the session completes.
|
|
1356
|
+
*/
|
|
1357
|
+
async postGitLabReply(event, runner, _repository) {
|
|
1358
|
+
try {
|
|
1359
|
+
// Get the last assistant message from the runner as the summary
|
|
1360
|
+
const messages = runner.getMessages();
|
|
1361
|
+
const lastAssistantMessage = [...messages]
|
|
1362
|
+
.reverse()
|
|
1363
|
+
.find((m) => m.type === "assistant");
|
|
1364
|
+
let summary = "Task completed. Please review the changes on this branch.";
|
|
1365
|
+
if (lastAssistantMessage &&
|
|
1366
|
+
lastAssistantMessage.type === "assistant" &&
|
|
1367
|
+
"message" in lastAssistantMessage) {
|
|
1368
|
+
const msg = lastAssistantMessage;
|
|
1369
|
+
const textBlock = msg.message.content?.find((block) => block.type === "text" && block.text);
|
|
1370
|
+
if (textBlock?.text) {
|
|
1371
|
+
summary = textBlock.text;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const projectId = extractProjectId(event);
|
|
1375
|
+
const mrIid = extractMRIid(event);
|
|
1376
|
+
const discussionId = extractDiscussionId(event);
|
|
1377
|
+
if (!mrIid) {
|
|
1378
|
+
this.logger.warn("Cannot post GitLab reply: no MR iid");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const token = event.accessToken || process.env.GITLAB_ACCESS_TOKEN;
|
|
1382
|
+
if (!token) {
|
|
1383
|
+
this.logger.warn("Cannot post GitLab reply: no access token or GITLAB_ACCESS_TOKEN configured");
|
|
1384
|
+
this.logger.debug(`Would have posted reply to ${extractProjectPath(event)}!${mrIid}: ${summary}`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (discussionId) {
|
|
1388
|
+
// Reply to the specific discussion thread
|
|
1389
|
+
await this.gitLabCommentService.postDiscussionReply({
|
|
1390
|
+
token,
|
|
1391
|
+
projectId,
|
|
1392
|
+
mrIid,
|
|
1393
|
+
discussionId,
|
|
1394
|
+
body: summary,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
// Post as a top-level MR note
|
|
1399
|
+
await this.gitLabCommentService.postMRNote({
|
|
1400
|
+
token,
|
|
1401
|
+
projectId,
|
|
1402
|
+
mrIid,
|
|
1403
|
+
body: summary,
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
this.logger.info(`Posted GitLab reply to ${extractProjectPath(event)}!${mrIid}`);
|
|
1407
|
+
}
|
|
1408
|
+
catch (error) {
|
|
1409
|
+
this.logger.error("Failed to post GitLab reply", error instanceof Error ? error : new Error(String(error)));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1012
1412
|
/**
|
|
1013
1413
|
* Compute the current status of the Cyrus process
|
|
1014
1414
|
* @returns "idle" if the process can be safely restarted, "busy" if work is in progress
|
|
@@ -1228,6 +1628,43 @@ ${taskSection}`;
|
|
|
1228
1628
|
this.logger.error(`Failed to re-run verifications:`, error);
|
|
1229
1629
|
}
|
|
1230
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Detect workspace token changes and update all dependent services.
|
|
1633
|
+
*
|
|
1634
|
+
* When an OAuth token is refreshed (at least once per day), the new token is
|
|
1635
|
+
* persisted to config.json which triggers the file watcher. This method
|
|
1636
|
+
* compares the previous in-memory tokens against the new config and calls
|
|
1637
|
+
* `setAccessToken()` on any affected `LinearIssueTrackerService` instances,
|
|
1638
|
+
* and pushes the updated workspace configs to `AttachmentService`.
|
|
1639
|
+
*/
|
|
1640
|
+
updateLinearWorkspaceTokens(newConfig) {
|
|
1641
|
+
const oldWorkspaces = this.config.linearWorkspaces ?? {};
|
|
1642
|
+
const newWorkspaces = newConfig.linearWorkspaces ?? {};
|
|
1643
|
+
let anyTokenChanged = false;
|
|
1644
|
+
for (const [workspaceId, newWsConfig] of Object.entries(newWorkspaces)) {
|
|
1645
|
+
const oldToken = oldWorkspaces[workspaceId]?.linearToken;
|
|
1646
|
+
const newToken = newWsConfig.linearToken;
|
|
1647
|
+
if (oldToken === newToken)
|
|
1648
|
+
continue;
|
|
1649
|
+
anyTokenChanged = true;
|
|
1650
|
+
// Update existing issue tracker in-place
|
|
1651
|
+
const issueTracker = this.issueTrackers.get(workspaceId);
|
|
1652
|
+
if (issueTracker) {
|
|
1653
|
+
issueTracker.setAccessToken(newToken);
|
|
1654
|
+
this.logger.info(`🔑 Updated Linear token for workspace ${workspaceId}`);
|
|
1655
|
+
}
|
|
1656
|
+
else if (this.config.platform !== "cli") {
|
|
1657
|
+
// Workspace is new — create a tracker for it
|
|
1658
|
+
const newIssueTracker = new LinearIssueTrackerService(new LinearClient({ accessToken: newToken }), this.buildOAuthConfig(workspaceId));
|
|
1659
|
+
this.issueTrackers.set(workspaceId, newIssueTracker);
|
|
1660
|
+
this.logger.info(`🔑 Created issue tracker for new workspace ${workspaceId}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (anyTokenChanged) {
|
|
1664
|
+
// Push refreshed workspace configs to AttachmentService
|
|
1665
|
+
this.attachmentService.setLinearWorkspaces(newWorkspaces);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1231
1668
|
/**
|
|
1232
1669
|
* Add new repositories to the running EdgeWorker
|
|
1233
1670
|
*/
|
|
@@ -2173,6 +2610,13 @@ ${taskSection}`;
|
|
|
2173
2610
|
finalClassification = routingDecision.classification;
|
|
2174
2611
|
log.info(`AI routing: ${routingDecision.classification} → ${finalProcedure.name}`);
|
|
2175
2612
|
}
|
|
2613
|
+
// Apply platform-specific subroutine substitution (e.g., gh-pr → glab-mr for GitLab repos)
|
|
2614
|
+
const hostingPlatform = primaryRepo.gitlabUrl
|
|
2615
|
+
? "gitlab"
|
|
2616
|
+
: primaryRepo.githubUrl
|
|
2617
|
+
? "github"
|
|
2618
|
+
: undefined;
|
|
2619
|
+
finalProcedure = applyPlatformSubroutines(finalProcedure, hostingPlatform);
|
|
2176
2620
|
// Initialize procedure metadata in session with final decision
|
|
2177
2621
|
this.procedureAnalyzer.initializeProcedureMetadata(session, finalProcedure);
|
|
2178
2622
|
// Post single procedure selection result (replaces ephemeral routing thought)
|
|
@@ -3605,6 +4049,13 @@ ${input.userComment}
|
|
|
3605
4049
|
finalClassification = routingDecision.classification;
|
|
3606
4050
|
this.logger.info(`AI routing: ${routingDecision.classification} → ${selectedProcedure.name}`);
|
|
3607
4051
|
}
|
|
4052
|
+
// Apply platform-specific subroutine substitution (e.g., gh-pr → glab-mr for GitLab repos)
|
|
4053
|
+
const hostingPlatform = repository.gitlabUrl
|
|
4054
|
+
? "gitlab"
|
|
4055
|
+
: repository.githubUrl
|
|
4056
|
+
? "github"
|
|
4057
|
+
: undefined;
|
|
4058
|
+
selectedProcedure = applyPlatformSubroutines(selectedProcedure, hostingPlatform);
|
|
3608
4059
|
// Initialize procedure metadata in session (resets currentSubroutine)
|
|
3609
4060
|
this.procedureAnalyzer.initializeProcedureMetadata(session, selectedProcedure);
|
|
3610
4061
|
// Post procedure selection result (replaces ephemeral routing thought)
|