From 09f6903ebb51a63dbb1f80807b6e83fd5394b750 Mon Sep 17 00:00:00 2001 From: Lauren ten Hoor Date: Tue, 10 Feb 2026 18:22:44 +0800 Subject: [PATCH] feat: include PR/MR URL in DEV completion notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds automatic PR/MR URL detection and inclusion in DEV completion announcements to provide better visibility into completed work. Changes: 1. TaskManager interface: - Added getMergedMRUrl() method to fetch PR/MR URL for an issue 2. GitHub/GitLab providers: - Implemented getMergedMRUrl() to find most recent merged PR/MR - Returns URL of PR/MR that references the issue number 3. task_complete tool: - Added optional prUrl parameter - Auto-detects PR/MR URL if not provided - Includes PR URL in announcement with šŸ”— prefix - Format: 'PR: https://github.com/...' 4. Role templates (dev.md): - Updated to instruct workers to include prUrl in task_complete - Documents that prUrl is optional (auto-detected as fallback) Example announcement: āœ… DEV done #101 — Added PR/MR URL to notifications šŸ”— PR: https://github.com/user/repo/pull/102 . Moved to QA queue. Addresses issue #101 --- lib/task-managers/github.ts | 31 ++++++++++++++++++++++++++++++ lib/task-managers/gitlab.ts | 32 +++++++++++++++++++++++++++++++ lib/task-managers/task-manager.ts | 3 +++ lib/tools/task-complete.ts | 30 ++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/lib/task-managers/github.ts b/lib/task-managers/github.ts index 704e4af..a648497 100644 --- a/lib/task-managers/github.ts +++ b/lib/task-managers/github.ts @@ -224,6 +224,37 @@ export class GitHubProvider implements TaskManager { } } + async getMergedMRUrl(issueId: number): Promise { + try { + const raw = await this.gh([ + "pr", "list", + "--state", "merged", + "--json", "number,title,body,url,mergedAt", + "--limit", "20", + ]); + const prs = JSON.parse(raw) as Array<{ + number: number; + title: string; + body: string; + url: string; + mergedAt: string; + }>; + + const pattern = `#${issueId}`; + + // Find the most recently merged PR that references this issue + // PRs are returned in reverse chronological order by default + const matchingPr = prs.find( + (pr) => + pr.title.includes(pattern) || (pr.body ?? "").includes(pattern), + ); + + return matchingPr?.url ?? null; + } catch { + return null; + } + } + async addComment(issueId: number, body: string): Promise { // Write body to temp file to preserve newlines const tempFile = join(tmpdir(), `devclaw-comment-${Date.now()}.md`); diff --git a/lib/task-managers/gitlab.ts b/lib/task-managers/gitlab.ts index 6d9ee44..93e6ab6 100644 --- a/lib/task-managers/gitlab.ts +++ b/lib/task-managers/gitlab.ts @@ -187,6 +187,38 @@ export class GitLabProvider implements TaskManager { } } + async getMergedMRUrl(issueId: number): Promise { + try { + const raw = await this.glab([ + "mr", "list", "--output", "json", "--state", "merged", + ]); + const mrs = JSON.parse(raw) as Array<{ + iid: number; + title: string; + description: string; + web_url: string; + merged_at: string; + }>; + + const pattern = `#${issueId}`; + + // Find the most recently merged MR that references this issue + // Sort by merged_at to get the most recent first + const matchingMr = mrs + .filter( + (mr) => + mr.title.includes(pattern) || (mr.description ?? "").includes(pattern), + ) + .sort((a, b) => + new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime() + )[0]; + + return matchingMr?.web_url ?? null; + } catch { + return null; + } + } + async addComment(issueId: number, body: string): Promise { // Write body to temp file to preserve newlines const tempFile = join(tmpdir(), `devclaw-comment-${Date.now()}.md`); diff --git a/lib/task-managers/task-manager.ts b/lib/task-managers/task-manager.ts index 8b4195f..e23de47 100644 --- a/lib/task-managers/task-manager.ts +++ b/lib/task-managers/task-manager.ts @@ -75,6 +75,9 @@ export interface TaskManager { /** Check if any merged MR/PR exists for a specific issue. */ hasMergedMR(issueId: number): Promise; + /** Get the URL of the most recently merged MR/PR for a specific issue. Returns null if not found. */ + getMergedMRUrl(issueId: number): Promise; + /** Add a comment to an issue. */ addComment(issueId: number, body: string): Promise; diff --git a/lib/tools/task-complete.ts b/lib/tools/task-complete.ts index 7465901..2801a46 100644 --- a/lib/tools/task-complete.ts +++ b/lib/tools/task-complete.ts @@ -57,6 +57,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) { type: "string", description: "Brief summary for group announcement", }, + prUrl: { + type: "string", + description: "Pull Request or Merge Request URL (optional, auto-detected if not provided)", + }, }, }, @@ -65,6 +69,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) { const result = params.result as "done" | "pass" | "fail" | "refine" | "blocked"; const groupId = params.projectGroupId as string; const summary = params.summary as string | undefined; + let prUrl = params.prUrl as string | undefined; const workspaceDir = ctx.workspaceDir; if (!workspaceDir) { @@ -133,11 +138,34 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) { output.gitPull = `warning: ${(err as Error).message}`; } + // Auto-detect PR/MR URL if not provided + if (!prUrl) { + try { + prUrl = await provider.getMergedMRUrl(issueId) ?? undefined; + } catch (err) { + // Ignore errors in PR URL detection + } + } + await deactivateWorker(workspaceDir, groupId, "dev"); await provider.transitionLabel(issueId, "Doing", "To Test"); output.labelTransition = "Doing → To Test"; - output.announcement = `āœ… DEV done #${issueId}${summary ? ` — ${summary}` : ""}. Moved to QA queue.`; + + // Build announcement with PR URL if available + let announcement = `āœ… DEV done #${issueId}`; + if (summary) { + announcement += ` — ${summary}`; + } + if (prUrl) { + announcement += `\nšŸ”— PR: ${prUrl}`; + } + announcement += `. Moved to QA queue.`; + + output.announcement = announcement; + if (prUrl) { + output.prUrl = prUrl; + } if (project.autoChain) { try {