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 {