From 1d3e3ee46be37f1ca2f95a858801f2bf66373caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Fri, 20 Mar 2026 11:48:13 -0600 Subject: [PATCH] feat(gsd): create draft PR on milestone completion when git.auto_pr enabled (#1627) Adds createDraftPR() to git-service.ts and hooks it into the milestone transition block in auto-loop.ts. Best-effort, non-fatal on failure. Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-loop.ts | 55 +++++++++++++++++++++ src/resources/extensions/gsd/git-service.ts | 24 +++++++++ 2 files changed, 79 insertions(+) diff --git a/src/resources/extensions/gsd/auto-loop.ts b/src/resources/extensions/gsd/auto-loop.ts index 06651f5d7..93220ee43 100644 --- a/src/resources/extensions/gsd/auto-loop.ts +++ b/src/resources/extensions/gsd/auto-loop.ts @@ -840,6 +840,25 @@ export async function autoLoop( // Worktree lifecycle on milestone transition — merge current, enter next deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui); + + // Opt-in: create draft PR on milestone completion + if (prefs?.git?.auto_pr) { + try { + const { createDraftPR } = await import("./git-service.js"); + const prUrl = createDraftPR( + s.basePath, + s.currentMilestoneId!, + `[GSD] ${s.currentMilestoneId} complete`, + `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`, + ); + if (prUrl) { + ctx.ui.notify(`Draft PR created: ${prUrl}`, "info"); + } + } catch { + // Non-fatal — PR creation is best-effort + } + } + deps.invalidateAllCaches(); state = await deps.deriveState(s.basePath); @@ -893,6 +912,24 @@ export async function autoLoop( // All milestones complete — merge milestone branch before stopping if (s.currentMilestoneId) { deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui); + + // Opt-in: create draft PR on milestone completion + if (prefs?.git?.auto_pr) { + try { + const { createDraftPR } = await import("./git-service.js"); + const prUrl = createDraftPR( + s.basePath, + s.currentMilestoneId, + `[GSD] ${s.currentMilestoneId} complete`, + `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`, + ); + if (prUrl) { + ctx.ui.notify(`Draft PR created: ${prUrl}`, "info"); + } + } catch { + // Non-fatal — PR creation is best-effort + } + } } deps.sendDesktopNotification( "GSD", @@ -974,6 +1011,24 @@ export async function autoLoop( // Milestone merge on complete (before closeout so branch state is clean) if (s.currentMilestoneId) { deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui); + + // Opt-in: create draft PR on milestone completion + if (prefs?.git?.auto_pr) { + try { + const { createDraftPR } = await import("./git-service.js"); + const prUrl = createDraftPR( + s.basePath, + s.currentMilestoneId, + `[GSD] ${s.currentMilestoneId} complete`, + `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`, + ); + if (prUrl) { + ctx.ui.notify(`Draft PR created: ${prUrl}`, "info"); + } + } catch { + // Non-fatal — PR creation is best-effort + } + } } deps.sendDesktopNotification( "GSD", diff --git a/src/resources/extensions/gsd/git-service.ts b/src/resources/extensions/gsd/git-service.ts index 094ac4352..7a7c25fbe 100644 --- a/src/resources/extensions/gsd/git-service.ts +++ b/src/resources/extensions/gsd/git-service.ts @@ -584,6 +584,30 @@ export class GitServiceImpl { } +// ─── Draft PR Creation ───────────────────────────────────────────────────── + +/** + * Create a draft pull request for a completed milestone using `gh pr create`. + * Returns the PR URL on success, or null on failure. + * Non-fatal: callers should treat failure as best-effort. + */ +export function createDraftPR( + basePath: string, + milestoneId: string, + title: string, + body: string, +): string | null { + try { + const result = execSync( + `gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`, + { cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV }, + ); + return result.trim(); + } catch { + return null; + } +} + // ─── Factory ─────────────────────────────────────────────────────────────── /** Create a GitServiceImpl with the current effective git preferences. */