From cf87569a14d6632719830cdebb452979189a5931 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 30 Dec 2025 00:06:30 -0800 Subject: [PATCH] fix: Connect gt done and mq submit to refinery mrqueue (gt-9mzd) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: gt done and mq submit created merge-request beads but did not write to the .beads/mq/ directory that the refinery Engineer polls for work. Changes: - done.go: Submit to mrqueue after creating MR bead - mq_submit.go: Submit to mrqueue after creating MR bead - mq_migrate.go: New command to migrate existing stale MR beads - mrqueue.go: Follow beads redirects for shared queue location The migration command allows recovery of existing stale MRs that were never processed because they only existed as beads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/done.go | 23 +++++ internal/cmd/mq_migrate.go | 175 ++++++++++++++++++++++++++++++++++++ internal/cmd/mq_submit.go | 23 +++++ internal/mrqueue/mrqueue.go | 33 ++++++- 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/mq_migrate.go diff --git a/internal/cmd/done.go b/internal/cmd/done.go index 9db409c1..8bb3b362 100644 --- a/internal/cmd/done.go +++ b/internal/cmd/done.go @@ -10,6 +10,7 @@ import ( "github.com/steveyegge/gastown/internal/events" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/mail" + "github.com/steveyegge/gastown/internal/mrqueue" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) @@ -163,6 +164,28 @@ func runDone(cmd *cobra.Command, args []string) error { } mrID = mrIssue.ID + // Also submit to mrqueue so refinery can process it + // The mrqueue is the work queue the refinery polls; beads are the audit record + mq, err := mrqueue.NewFromWorkdir(cwd) + if err != nil { + // Non-fatal: bead was created, just warn about queue + style.PrintWarning("could not access merge queue: %v", err) + } else { + mqEntry := &mrqueue.MR{ + ID: mrID, + Branch: branch, + Target: target, + SourceIssue: issueID, + Worker: worker, + Rig: rigName, + Title: title, + Priority: priority, + } + if err := mq.Submit(mqEntry); err != nil { + style.PrintWarning("could not submit to merge queue: %v", err) + } + } + // Success output fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓")) fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) diff --git a/internal/cmd/mq_migrate.go b/internal/cmd/mq_migrate.go new file mode 100644 index 00000000..19284a96 --- /dev/null +++ b/internal/cmd/mq_migrate.go @@ -0,0 +1,175 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/mrqueue" + "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/workspace" +) + +var ( + mqMigrateDryRun bool +) + +var mqMigrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Migrate stale merge-request beads to the mrqueue", + Long: `Migrate existing merge-request beads to the mrqueue. + +This command finds merge-request beads that were created before the mrqueue +integration and adds them to the refinery's work queue (.beads/mq/). + +Use this to recover stale MRs that the refinery wasn't processing because +they only existed as beads. + +Examples: + gt mq migrate # Migrate all stale MRs + gt mq migrate --dry-run # Preview what would be migrated`, + RunE: runMqMigrate, +} + +func init() { + mqMigrateCmd.Flags().BoolVar(&mqMigrateDryRun, "dry-run", false, "Preview only, don't actually migrate") + mqCmd.AddCommand(mqMigrateCmd) +} + +func runMqMigrate(cmd *cobra.Command, args []string) error { + // Find workspace + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Find current rig + rigName, _, err := findCurrentRig(townRoot) + if err != nil { + return err + } + + // Initialize beads + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + bd := beads.New(cwd) + + // Initialize mrqueue + mq, err := mrqueue.NewFromWorkdir(cwd) + if err != nil { + return fmt.Errorf("accessing merge queue: %w", err) + } + + // Get existing mrqueue entries to avoid duplicates + existingMRs, err := mq.List() + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("listing existing queue: %w", err) + } + existingIDs := make(map[string]bool) + for _, mr := range existingMRs { + existingIDs[mr.ID] = true + } + + // List all open merge-request beads + // Note: beads.List() with default ListOptions filters for P0 only (Priority default is 0) + // So we use Priority: -1 to indicate no filter + allIssues, err := bd.List(beads.ListOptions{Priority: -1}) + if err != nil { + return fmt.Errorf("listing all beads: %w", err) + } + + // Filter for open merge-requests + var issues []*beads.Issue + for _, issue := range allIssues { + if issue.Type == "merge-request" && issue.Status == "open" { + issues = append(issues, issue) + } + } + + if len(issues) == 0 { + fmt.Println("No stale merge-request beads found.") + return nil + } + + // Filter to only those not already in mrqueue + var toMigrate []*beads.Issue + for _, issue := range issues { + if !existingIDs[issue.ID] { + toMigrate = append(toMigrate, issue) + } + } + + if len(toMigrate) == 0 { + fmt.Println("All merge-request beads already in mrqueue.") + return nil + } + + fmt.Printf("Found %d stale merge-request bead(s) to migrate:\n\n", len(toMigrate)) + + migrated := 0 + for _, issue := range toMigrate { + // Parse MR fields from bead description + mrFields := beads.ParseMRFields(issue) + if mrFields == nil { + fmt.Printf(" %s %s - skipping (no MR fields in description)\n", + style.Dim.Render("⚠"), issue.ID) + continue + } + + // Extract worker from description + worker := mrFields.Worker + if worker == "" { + // Try to extract from branch name + if strings.HasPrefix(mrFields.Branch, "polecat/") { + parts := strings.SplitN(mrFields.Branch, "/", 3) + if len(parts) >= 2 { + worker = parts[1] + } + } + } + + if mqMigrateDryRun { + fmt.Printf(" %s %s - %s (branch: %s, target: %s)\n", + style.Bold.Render("→"), issue.ID, issue.Title, + mrFields.Branch, mrFields.Target) + } else { + // Create mrqueue entry + mqEntry := &mrqueue.MR{ + ID: issue.ID, + Branch: mrFields.Branch, + Target: mrFields.Target, + SourceIssue: mrFields.SourceIssue, + Worker: worker, + Rig: rigName, + Title: issue.Title, + Priority: issue.Priority, + } + + if err := mq.Submit(mqEntry); err != nil { + fmt.Printf(" %s %s - failed: %v\n", + style.Dim.Render("✗"), issue.ID, err) + continue + } + + fmt.Printf(" %s %s - %s\n", + style.Bold.Render("✓"), issue.ID, issue.Title) + migrated++ + } + } + + fmt.Println() + if mqMigrateDryRun { + fmt.Printf("Dry run: would migrate %d merge-request(s)\n", len(toMigrate)) + fmt.Println("Run without --dry-run to perform migration.") + } else { + fmt.Printf("%s Migrated %d merge-request(s) to mrqueue\n", + style.Bold.Render("✓"), migrated) + fmt.Println("The refinery will process them on its next poll cycle.") + } + + return nil +} diff --git a/internal/cmd/mq_submit.go b/internal/cmd/mq_submit.go index cdc49eb7..9d50b52e 100644 --- a/internal/cmd/mq_submit.go +++ b/internal/cmd/mq_submit.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/git" + "github.com/steveyegge/gastown/internal/mrqueue" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) @@ -149,6 +150,28 @@ func runMqSubmit(cmd *cobra.Command, args []string) error { return fmt.Errorf("creating merge request bead: %w", err) } + // Also submit to mrqueue so refinery can process it + // The mrqueue is the work queue the refinery polls; beads are the audit record + mq, err := mrqueue.NewFromWorkdir(cwd) + if err != nil { + // Non-fatal: bead was created, just warn about queue + style.PrintWarning("could not access merge queue: %v", err) + } else { + mqEntry := &mrqueue.MR{ + ID: mrIssue.ID, + Branch: branch, + Target: target, + SourceIssue: issueID, + Worker: worker, + Rig: rigName, + Title: title, + Priority: priority, + } + if err := mq.Submit(mqEntry); err != nil { + style.PrintWarning("could not submit to merge queue: %v", err) + } + } + // Success output fmt.Printf("%s Submitted to merge queue\n", style.Bold.Render("✓")) fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrIssue.ID)) diff --git a/internal/mrqueue/mrqueue.go b/internal/mrqueue/mrqueue.go index 5ae5cac8..7565caa0 100644 --- a/internal/mrqueue/mrqueue.go +++ b/internal/mrqueue/mrqueue.go @@ -41,13 +41,16 @@ func New(rigPath string) *Queue { } // NewFromWorkdir creates a queue by finding the rig root from a working directory. +// It follows beads redirects to ensure all clones use the same shared mrqueue. func NewFromWorkdir(workdir string) (*Queue, error) { // Walk up to find .beads or rig root dir := workdir for { beadsDir := filepath.Join(dir, ".beads") if info, err := os.Stat(beadsDir); err == nil && info.IsDir() { - return &Queue{dir: filepath.Join(beadsDir, "mq")}, nil + // Check for redirect and follow it + finalDir := resolveBeadsRedirect(beadsDir) + return &Queue{dir: filepath.Join(finalDir, "mq")}, nil } parent := filepath.Dir(dir) @@ -58,6 +61,34 @@ func NewFromWorkdir(workdir string) (*Queue, error) { } } +// resolveBeadsRedirect follows beads redirect files to find the final directory. +// Returns the original dir if no redirect or on error. +func resolveBeadsRedirect(beadsDir string) string { + redirectPath := filepath.Join(beadsDir, "redirect") + data, err := os.ReadFile(redirectPath) + if err != nil { + return beadsDir // No redirect file + } + + target := strings.TrimSpace(string(data)) + if target == "" { + return beadsDir + } + + // Resolve relative path from beadsDir's parent + if !filepath.IsAbs(target) { + target = filepath.Join(filepath.Dir(beadsDir), target) + } + + // Clean and verify the target exists + target = filepath.Clean(target) + if info, err := os.Stat(target); err == nil && info.IsDir() { + return target + } + + return beadsDir // Target doesn't exist, use original +} + // EnsureDir creates the MQ directory if it doesn't exist. func (q *Queue) EnsureDir() error { return os.MkdirAll(q.dir, 0755)