fix: Connect gt done and mq submit to refinery mrqueue (gt-9mzd)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/events"
|
"github.com/steveyegge/gastown/internal/events"
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
"github.com/steveyegge/gastown/internal/mail"
|
"github.com/steveyegge/gastown/internal/mail"
|
||||||
|
"github.com/steveyegge/gastown/internal/mrqueue"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
@@ -163,6 +164,28 @@ func runDone(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
mrID = mrIssue.ID
|
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
|
// Success output
|
||||||
fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓"))
|
||||||
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID))
|
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID))
|
||||||
|
|||||||
175
internal/cmd/mq_migrate.go
Normal file
175
internal/cmd/mq_migrate.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
|
"github.com/steveyegge/gastown/internal/mrqueue"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"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)
|
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
|
// Success output
|
||||||
fmt.Printf("%s Submitted to merge queue\n", style.Bold.Render("✓"))
|
fmt.Printf("%s Submitted to merge queue\n", style.Bold.Render("✓"))
|
||||||
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrIssue.ID))
|
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrIssue.ID))
|
||||||
|
|||||||
@@ -41,13 +41,16 @@ func New(rigPath string) *Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFromWorkdir creates a queue by finding the rig root from a working directory.
|
// 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) {
|
func NewFromWorkdir(workdir string) (*Queue, error) {
|
||||||
// Walk up to find .beads or rig root
|
// Walk up to find .beads or rig root
|
||||||
dir := workdir
|
dir := workdir
|
||||||
for {
|
for {
|
||||||
beadsDir := filepath.Join(dir, ".beads")
|
beadsDir := filepath.Join(dir, ".beads")
|
||||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
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)
|
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.
|
// EnsureDir creates the MQ directory if it doesn't exist.
|
||||||
func (q *Queue) EnsureDir() error {
|
func (q *Queue) EnsureDir() error {
|
||||||
return os.MkdirAll(q.dir, 0755)
|
return os.MkdirAll(q.dir, 0755)
|
||||||
|
|||||||
Reference in New Issue
Block a user