diff --git a/apps/open-swe/src/__tests__/extract-linked-issues.test.ts b/apps/open-swe/src/__tests__/extract-linked-issues.test.ts new file mode 100644 index 00000000..15bf169a --- /dev/null +++ b/apps/open-swe/src/__tests__/extract-linked-issues.test.ts @@ -0,0 +1,121 @@ +import { extractLinkedIssues } from "../routes/github/utils.js"; + +describe("extractLinkedIssues", () => { + it("should extract issues with 'fixes #number' format", () => { + const prBody = "This PR fixes #123 and also fixes #456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should extract issues with 'fixes: #number' format", () => { + const prBody = "This PR fixes: #123 and also fixes: #456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should extract issues with mixed formats", () => { + const prBody = "This PR fixes #123 and also fixes: #456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should extract issues with 'closes' keyword", () => { + const prBody = "closes #789 and closes: #101"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([789, 101]); + }); + + it("should extract issues with 'resolves' keyword", () => { + const prBody = "resolves #999 and resolves: #888"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([999, 888]); + }); + + it("should extract issues with singular forms", () => { + const prBody = "fix #111, close #222, resolve #333"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([111, 222, 333]); + }); + + it("should extract issues with singular forms and colon", () => { + const prBody = "fix: #111, close: #222, resolve: #333"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([111, 222, 333]); + }); + + it("should handle case insensitive keywords", () => { + const prBody = "FIXES #123, Closes: #456, ResolveS #789"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456, 789]); + }); + + it("should remove duplicate issue numbers", () => { + const prBody = "fixes #123, closes #123, resolves: #123"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123]); + }); + + it("should handle multiple spaces and whitespace variations", () => { + const prBody = "fixes #123 and closes: #456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should handle colon with no spaces", () => { + const prBody = "fixes:#123 and closes:#456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should handle colon with spaces on both sides", () => { + const prBody = "fixes : #123 and closes : #456"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456]); + }); + + it("should return empty array when no linked issues found", () => { + const prBody = + "This is just a regular PR description with no linked issues"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([]); + }); + + it("should ignore partial matches", () => { + const prBody = "This prefixes #123 but doesn't actually fix it"; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([]); + }); + + it("should handle multiline PR bodies", () => { + const prBody = ` + ## Summary + This PR fixes several issues + + fixes: #123 + closes #456 + + ## Additional Notes + Also resolves: #789 + `; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([123, 456, 789]); + }); + + it("should handle complex PR body with mixed content", () => { + const prBody = ` + # Bug Fix PR + + This PR addresses multiple issues: + - fixes #100 (memory leak) + - closes: #200 (UI bug) + - resolves #300 (performance issue) + + ## Testing + Tested with issue #400 but doesn't fix it yet. + + Fixes: #500 + `; + const result = extractLinkedIssues(prBody); + expect(result).toEqual([100, 200, 300, 500]); + }); +}); diff --git a/apps/open-swe/src/routes/github/constants.ts b/apps/open-swe/src/routes/github/constants.ts new file mode 100644 index 00000000..ca4cbc19 --- /dev/null +++ b/apps/open-swe/src/routes/github/constants.ts @@ -0,0 +1,3 @@ +export const GITHUB_TRIGGER_USERNAME = process.env.GITHUB_TRIGGER_USERNAME + ? `@${process.env.GITHUB_TRIGGER_USERNAME}` + : "@open-swe"; diff --git a/apps/open-swe/src/routes/github/pr-webhook-handler-base.ts b/apps/open-swe/src/routes/github/pr-webhook-handler-base.ts index 058ac68c..c53acaa8 100644 --- a/apps/open-swe/src/routes/github/pr-webhook-handler-base.ts +++ b/apps/open-swe/src/routes/github/pr-webhook-handler-base.ts @@ -5,7 +5,7 @@ import { import { RequestSource } from "../../constants.js"; import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types"; import { - mentionsOpenSWE, + mentionsGitHubUserForTrigger, extractLinkedIssues, getPrContext, convertPRPayloadToPullRequestObj, @@ -19,6 +19,7 @@ import { } from "./types.js"; import { GitHubPullRequestGet } from "../../utils/github/types.js"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { GITHUB_TRIGGER_USERNAME } from "./constants.js"; export interface PRWebhookContext extends WebhookHandlerContext { prNumber: number; @@ -32,8 +33,10 @@ export abstract class PRWebhookHandlerBase extends WebhookHandlerBase { content: string, logContext: string, ): boolean { - if (!mentionsOpenSWE(content)) { - this.logger.info(`${logContext} does not mention @open-swe, skipping`); + if (!mentionsGitHubUserForTrigger(content)) { + this.logger.info( + `${logContext} does not mention ${GITHUB_TRIGGER_USERNAME}, skipping`, + ); return false; } return true; diff --git a/apps/open-swe/src/routes/github/prompts.ts b/apps/open-swe/src/routes/github/prompts.ts index 2eb66600..8051f7da 100644 --- a/apps/open-swe/src/routes/github/prompts.ts +++ b/apps/open-swe/src/routes/github/prompts.ts @@ -11,10 +11,11 @@ import { SimplePullRequestReview, SimpleTriggerComment, } from "./types.js"; +import { GITHUB_TRIGGER_USERNAME } from "./constants.js"; // For PR review triggers const PR_REVIEW_TRIGGER_PROMPT = ` -You're tasked with resolving all of the relevant comments/reviews which were left on this pull request. +You're tasked with resolving all of the relevant comments/reviews which were left on this pull request. The user has tagged you (${GITHUB_TRIGGER_USERNAME}) in a review, meaning they want you to resolve the review and all of its comments for them. For each comment, determine whether or not it needs a code change, and if so update the code to properly resolve the comment. IMPORTANT: Remember that some comments might already be resolved, so don't blindly make changes based on the comments alone. You mainly care about the actual PR review which was left on the PR. @@ -87,7 +88,7 @@ IMPORTANT: The comments in the reviews should take precedence over the comments // For PR review comment triggers const PR_REVIEW_COMMENT_TRIGGER_PROMPT = ` -You're tasked with resolving the pull request review comment which was left on this PR, and you (open-swe) were tagged in. +You're tasked with resolving the pull request review comment which was left on this PR, and you (${GITHUB_TRIGGER_USERNAME}) were tagged in. For the review comment, determine whether or not it needs a code change, and if so update the code to properly resolve the comment. IMPORTANT: Remember that some comments might already be resolved, so don't blindly make changes based on the comments alone. You mainly care about the actual PR review which was left on the PR. @@ -159,7 +160,7 @@ IMPORTANT: The review comment should take precedence over the comments on the li // For PR comment triggers const PR_COMMENT_TRIGGER_PROMPT = ` -The user has tagged you (open-swe) in a comment on this pull request. Your task is to resolve their comment in the simplest way possible. +The user has tagged you (${GITHUB_TRIGGER_USERNAME}) in a comment on this pull request. Your task is to resolve their comment in the simplest way possible. Determine whether or not the comment requires a code change, and if so update the code to properly resolve the comment. After making a code change ensure you reply to the comment which requested the change using the '${createReplyToCommentToolFields().name}' tool. This message should be very short and to the point. diff --git a/apps/open-swe/src/routes/github/pull-request-comment.ts b/apps/open-swe/src/routes/github/pull-request-comment.ts index dafcf205..ac4b29c2 100644 --- a/apps/open-swe/src/routes/github/pull-request-comment.ts +++ b/apps/open-swe/src/routes/github/pull-request-comment.ts @@ -6,6 +6,7 @@ import { constructLinkToPRComment } from "./utils.js"; import { PullRequestReviewTriggerData } from "./types.js"; import { createPromptFromPRCommentTrigger } from "./prompts.js"; import { getRandomWebhookMessage } from "./webhook-messages.js"; +import { GITHUB_TRIGGER_USERNAME } from "./constants.js"; class PRCommentWebhookHandler extends PRWebhookHandlerBase { constructor() { @@ -45,7 +46,7 @@ class PRCommentWebhookHandler extends PRWebhookHandlerBase { } this.logger.info( - `@open-swe mentioned in PR #${payload.issue.number} comment`, + `${GITHUB_TRIGGER_USERNAME} mentioned in PR #${payload.issue.number} comment`, { commentId: payload.comment.id, author: payload.comment.user?.login, diff --git a/apps/open-swe/src/routes/github/pull-request-review-comment.ts b/apps/open-swe/src/routes/github/pull-request-review-comment.ts index a9d04525..b7e79270 100644 --- a/apps/open-swe/src/routes/github/pull-request-review-comment.ts +++ b/apps/open-swe/src/routes/github/pull-request-review-comment.ts @@ -6,6 +6,7 @@ import { constructLinkToPRReviewComment } from "./utils.js"; import { PullRequestReviewTriggerData } from "./types.js"; import { createPromptFromPRReviewCommentTrigger } from "./prompts.js"; import { getRandomWebhookMessage } from "./webhook-messages.js"; +import { GITHUB_TRIGGER_USERNAME } from "./constants.js"; class PRReviewCommentWebhookHandler extends PRWebhookHandlerBase { constructor() { @@ -40,7 +41,7 @@ class PRReviewCommentWebhookHandler extends PRWebhookHandlerBase { } this.logger.info( - `@open-swe mentioned in PR #${payload.pull_request.number} review comment`, + `${GITHUB_TRIGGER_USERNAME} mentioned in PR #${payload.pull_request.number} review comment`, { commentId: payload.comment.id, author: payload.comment.user?.login, diff --git a/apps/open-swe/src/routes/github/pull-request-review.ts b/apps/open-swe/src/routes/github/pull-request-review.ts index 2372f6ad..6cc0eb11 100644 --- a/apps/open-swe/src/routes/github/pull-request-review.ts +++ b/apps/open-swe/src/routes/github/pull-request-review.ts @@ -6,6 +6,7 @@ import { constructLinkToPRReview } from "./utils.js"; import { PullRequestReviewTriggerData } from "./types.js"; import { createPromptFromPRReviewTrigger } from "./prompts.js"; import { getRandomWebhookMessage } from "./webhook-messages.js"; +import { GITHUB_TRIGGER_USERNAME } from "./constants.js"; class PRReviewWebhookHandler extends PRWebhookHandlerBase { constructor() { @@ -40,7 +41,7 @@ class PRReviewWebhookHandler extends PRWebhookHandlerBase { } this.logger.info( - `@open-swe mentioned in PR #${payload.pull_request.number} review`, + `${GITHUB_TRIGGER_USERNAME} mentioned in PR #${payload.pull_request.number} review`, { reviewId: payload.review.id, author: payload.review.user?.login, diff --git a/apps/open-swe/src/routes/github/utils.ts b/apps/open-swe/src/routes/github/utils.ts index f447e134..c53de1d1 100644 --- a/apps/open-swe/src/routes/github/utils.ts +++ b/apps/open-swe/src/routes/github/utils.ts @@ -37,15 +37,14 @@ export function createDevMetadataComment(runId: string, threadId: string) { `; } -export function mentionsOpenSWE(commentBody: string): boolean { +export function mentionsGitHubUserForTrigger(commentBody: string): boolean { return /@open-swe\b/.test(commentBody); } export function extractLinkedIssues(prBody: string): number[] { // Look for common patterns like "fixes #123", "closes #456", "resolves #789" const patterns = [ - /(?:fixes?|closes?|resolves?)\s+#(\d+)/gi, - /(?:fix|close|resolve)\s+#(\d+)/gi, + /\b(?:fixes?|closes?|resolves?|fix|close|resolve)(?:\s*:\s*|\s+)#(\d+)/gi, ]; const issueNumbers: number[] = [];