From c2a33be4e60cb2d9bc0e820dec8f4c70027a44ec Mon Sep 17 00:00:00 2001 From: gastown/polecats/capable Date: Tue, 30 Dec 2025 22:12:30 -0800 Subject: [PATCH] Make gt done MR creation idempotent (gt-svdsy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add FindMRForBranch helper to check for existing MR beads before creating. If an MR already exists for the branch, skip creation and reuse it. This makes gt done safe to re-run if interrupted mid-execution. Implements Option C from gt-svdsy: idempotent operations that check if already done before doing, making it safe to retry. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/beads.go | 25 +++++++++++++++++++ internal/cmd/done.go | 54 ++++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 139f8ce9..3384d611 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -1118,3 +1118,28 @@ func (b *Beads) GetRoleConfig(roleBeadID string) (*RoleConfig, error) { return ParseRoleConfig(issue.Description), nil } + +// FindMRForBranch searches for an existing merge-request bead for the given branch. +// Returns the MR bead if found, nil if not found. +// This enables idempotent `gt done` - if an MR already exists, we skip creation. +func (b *Beads) FindMRForBranch(branch string) (*Issue, error) { + // List all merge-request beads (open status only - closed MRs are already processed) + issues, err := b.List(ListOptions{ + Status: "open", + Type: "merge-request", + }) + if err != nil { + return nil, err + } + + // Search for one matching this branch + // MR description format: "branch: \ntarget: ..." + branchPrefix := "branch: " + branch + "\n" + for _, issue := range issues { + if strings.HasPrefix(issue.Description, branchPrefix) { + return issue, nil + } + } + + return nil, nil +} diff --git a/internal/cmd/done.go b/internal/cmd/done.go index 2724fa0d..2767f857 100644 --- a/internal/cmd/done.go +++ b/internal/cmd/done.go @@ -153,29 +153,43 @@ func runDone(cmd *cobra.Command, args []string) error { } } - // Build MR bead title and description - title := fmt.Sprintf("Merge: %s", issueID) - description := fmt.Sprintf("branch: %s\ntarget: %s\nsource_issue: %s\nrig: %s", - branch, target, issueID, rigName) - if worker != "" { - description += fmt.Sprintf("\nworker: %s", worker) - } - - // Create MR bead (ephemeral wisp - will be cleaned up after merge) - mrIssue, err := bd.Create(beads.CreateOptions{ - Title: title, - Type: "merge-request", - Priority: priority, - Description: description, - }) + // Check if MR bead already exists for this branch (idempotency) + existingMR, err := bd.FindMRForBranch(branch) if err != nil { - return fmt.Errorf("creating merge request bead: %w", err) + style.PrintWarning("could not check for existing MR: %v", err) + // Continue with creation attempt - Create will fail if duplicate } - mrID = mrIssue.ID - // Success output - fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓")) - fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + if existingMR != nil { + // MR already exists - use it instead of creating a new one + mrID = existingMR.ID + fmt.Printf("%s MR already exists (idempotent)\n", style.Bold.Render("✓")) + fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + } else { + // Build MR bead title and description + title := fmt.Sprintf("Merge: %s", issueID) + description := fmt.Sprintf("branch: %s\ntarget: %s\nsource_issue: %s\nrig: %s", + branch, target, issueID, rigName) + if worker != "" { + description += fmt.Sprintf("\nworker: %s", worker) + } + + // Create MR bead (ephemeral wisp - will be cleaned up after merge) + mrIssue, err := bd.Create(beads.CreateOptions{ + Title: title, + Type: "merge-request", + Priority: priority, + Description: description, + }) + if err != nil { + return fmt.Errorf("creating merge request bead: %w", err) + } + mrID = mrIssue.ID + + // Success output + fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓")) + fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + } fmt.Printf(" Source: %s\n", branch) fmt.Printf(" Target: %s\n", target) fmt.Printf(" Issue: %s\n", issueID)