package cmd import ( "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/refinery" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/style" ) // MQ command flags var ( // Submit flags mqSubmitBranch string mqSubmitIssue string mqSubmitEpic string mqSubmitPriority int mqSubmitNoCleanup bool // Retry flags mqRetryNow bool // Reject flags mqRejectReason string mqRejectNotify bool // List command flags mqListReady bool mqListStatus string mqListWorker string mqListEpic string mqListJSON bool // Status command flags mqStatusJSON bool // Integration land flags mqIntegrationLandForce bool mqIntegrationLandSkipTests bool mqIntegrationLandDryRun bool // Integration status flags mqIntegrationStatusJSON bool ) var mqCmd = &cobra.Command{ Use: "mq", Short: "Merge queue operations", Long: `Manage the merge queue for a rig. The merge queue tracks work branches from polecats waiting to be merged. Use these commands to view, submit, retry, and manage merge requests.`, } var mqSubmitCmd = &cobra.Command{ Use: "submit", Short: "Submit current branch to the merge queue", Long: `Submit the current branch to the merge queue. Creates a merge-request bead that will be processed by the Refinery. Auto-detection: - Branch: current git branch - Issue: parsed from branch name (e.g., polecat/Nux/gt-xyz → gt-xyz) - Worker: parsed from branch name - Rig: detected from current directory - Target: automatically determined (see below) - Priority: inherited from source issue Target branch auto-detection: 1. If --epic is specified: target integration/ 2. If source issue has a parent epic with integration/ branch: target it 3. Otherwise: target main This ensures batch work on epics automatically flows to integration branches. Polecat auto-cleanup: When run from a polecat work branch (polecat//), this command automatically triggers polecat shutdown after submitting the MR. The polecat sends a lifecycle request to its Witness and waits for termination. Use --no-cleanup to disable this behavior (e.g., if you want to submit multiple MRs or continue working). Examples: gt mq submit # Auto-detect everything + auto-cleanup gt mq submit --issue gt-abc # Explicit issue gt mq submit --epic gt-xyz # Target integration branch explicitly gt mq submit --priority 0 # Override priority (P0) gt mq submit --no-cleanup # Submit without auto-cleanup`, RunE: runMqSubmit, } var mqRetryCmd = &cobra.Command{ Use: "retry ", Short: "Retry a failed merge request", Long: `Retry a failed merge request. Resets a failed MR so it can be processed again by the refinery. The MR must be in a failed state (open with an error). Examples: gt mq retry gastown gt-mr-abc123 gt mq retry gastown gt-mr-abc123 --now`, Args: cobra.ExactArgs(2), RunE: runMQRetry, } var mqListCmd = &cobra.Command{ Use: "list ", Short: "Show the merge queue", Long: `Show the merge queue for a rig. Lists all pending merge requests waiting to be processed. Output format: ID STATUS PRIORITY BRANCH WORKER AGE gt-mr-001 ready P0 polecat/Nux/gt-xyz Nux 5m gt-mr-002 in_progress P1 polecat/Toast/gt-abc Toast 12m gt-mr-003 blocked P1 polecat/Capable/gt-def Capable 8m (waiting on gt-mr-001) Examples: gt mq list gastown gt mq list gastown --ready gt mq list gastown --status=open gt mq list gastown --worker=Nux`, Args: cobra.ExactArgs(1), RunE: runMQList, } var mqRejectCmd = &cobra.Command{ Use: "reject ", Short: "Reject a merge request", Long: `Manually reject a merge request. This closes the MR with a 'rejected' status without merging. The source issue is NOT closed (work is not done). Examples: gt mq reject gastown polecat/Nux/gt-xyz --reason "Does not meet requirements" gt mq reject gastown mr-Nux-12345 --reason "Superseded by other work" --notify`, Args: cobra.ExactArgs(2), RunE: runMQReject, } var mqStatusCmd = &cobra.Command{ Use: "status ", Short: "Show detailed merge request status", Long: `Display detailed information about a merge request. Shows all MR fields, current status with timestamps, dependencies, blockers, and processing history. Example: gt mq status gt-mr-abc123`, Args: cobra.ExactArgs(1), RunE: runMqStatus, } var mqIntegrationCmd = &cobra.Command{ Use: "integration", Short: "Manage integration branches for epics", Long: `Manage integration branches for batch work on epics. Integration branches allow multiple MRs for an epic to target a shared branch instead of main. After all epic work is complete, the integration branch is landed to main as a single atomic unit. Commands: create Create an integration branch for an epic land Merge integration branch to main status Show integration branch status`, } var mqIntegrationCreateCmd = &cobra.Command{ Use: "create ", Short: "Create an integration branch for an epic", Long: `Create an integration branch for batch work on an epic. Creates a branch named integration/ from main and pushes it to origin. Future MRs for this epic's children can target this branch. Actions: 1. Verify epic exists 2. Create branch integration/ from main 3. Push to origin 4. Store integration branch info in epic metadata Example: gt mq integration create gt-auth-epic # Creates integration/gt-auth-epic from main`, Args: cobra.ExactArgs(1), RunE: runMqIntegrationCreate, } var mqIntegrationLandCmd = &cobra.Command{ Use: "land ", Short: "Merge integration branch to main", Long: `Merge an epic's integration branch to main. Lands all work for an epic by merging its integration branch to main as a single atomic merge commit. Actions: 1. Verify all MRs targeting integration/ are merged 2. Verify integration branch exists 3. Merge integration/ to main (--no-ff) 4. Run tests on main 5. Push to origin 6. Delete integration branch 7. Update epic status Options: --force Land even if some MRs still open --skip-tests Skip test run --dry-run Preview only, make no changes Examples: gt mq integration land gt-auth-epic gt mq integration land gt-auth-epic --dry-run gt mq integration land gt-auth-epic --force --skip-tests`, Args: cobra.ExactArgs(1), RunE: runMqIntegrationLand, } var mqIntegrationStatusCmd = &cobra.Command{ Use: "status ", Short: "Show integration branch status for an epic", Long: `Display the status of an integration branch. Shows: - Integration branch name and creation date - Number of commits ahead of main - Merged MRs (closed, targeting integration branch) - Pending MRs (open, targeting integration branch) Example: gt mq integration status gt-auth-epic`, Args: cobra.ExactArgs(1), RunE: runMqIntegrationStatus, } func init() { // Submit flags mqSubmitCmd.Flags().StringVar(&mqSubmitBranch, "branch", "", "Source branch (default: current branch)") mqSubmitCmd.Flags().StringVar(&mqSubmitIssue, "issue", "", "Source issue ID (default: parse from branch name)") mqSubmitCmd.Flags().StringVar(&mqSubmitEpic, "epic", "", "Target epic's integration branch instead of main") mqSubmitCmd.Flags().IntVarP(&mqSubmitPriority, "priority", "p", -1, "Override priority (0-4, default: inherit from issue)") mqSubmitCmd.Flags().BoolVar(&mqSubmitNoCleanup, "no-cleanup", false, "Don't auto-cleanup after submit (for polecats)") // Retry flags mqRetryCmd.Flags().BoolVar(&mqRetryNow, "now", false, "Immediately process instead of waiting for refinery loop") // List flags mqListCmd.Flags().BoolVar(&mqListReady, "ready", false, "Show only ready-to-merge (no blockers)") mqListCmd.Flags().StringVar(&mqListStatus, "status", "", "Filter by status (open, in_progress, closed)") mqListCmd.Flags().StringVar(&mqListWorker, "worker", "", "Filter by worker name") mqListCmd.Flags().StringVar(&mqListEpic, "epic", "", "Show MRs targeting integration/") mqListCmd.Flags().BoolVar(&mqListJSON, "json", false, "Output as JSON") // Reject flags mqRejectCmd.Flags().StringVarP(&mqRejectReason, "reason", "r", "", "Reason for rejection (required)") mqRejectCmd.Flags().BoolVar(&mqRejectNotify, "notify", false, "Send mail notification to worker") _ = mqRejectCmd.MarkFlagRequired("reason") // Status flags mqStatusCmd.Flags().BoolVar(&mqStatusJSON, "json", false, "Output as JSON") // Add subcommands mqCmd.AddCommand(mqSubmitCmd) mqCmd.AddCommand(mqRetryCmd) mqCmd.AddCommand(mqListCmd) mqCmd.AddCommand(mqRejectCmd) mqCmd.AddCommand(mqStatusCmd) // Integration branch subcommands mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd) // Integration land flags mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandForce, "force", false, "Land even if some MRs still open") mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandSkipTests, "skip-tests", false, "Skip test run") mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandDryRun, "dry-run", false, "Preview only, make no changes") mqIntegrationCmd.AddCommand(mqIntegrationLandCmd) // Integration status flags mqIntegrationStatusCmd.Flags().BoolVar(&mqIntegrationStatusJSON, "json", false, "Output as JSON") mqIntegrationCmd.AddCommand(mqIntegrationStatusCmd) mqCmd.AddCommand(mqIntegrationCmd) rootCmd.AddCommand(mqCmd) } // findCurrentRig determines the current rig from the working directory. // Returns the rig name and rig object, or an error if not in a rig. func findCurrentRig(townRoot string) (string, *rig.Rig, error) { cwd, err := os.Getwd() if err != nil { return "", nil, fmt.Errorf("getting current directory: %w", err) } // Get relative path from town root to cwd relPath, err := filepath.Rel(townRoot, cwd) if err != nil { return "", nil, fmt.Errorf("computing relative path: %w", err) } // The first component of the relative path should be the rig name parts := strings.Split(relPath, string(filepath.Separator)) if len(parts) == 0 || parts[0] == "" || parts[0] == "." { return "", nil, fmt.Errorf("not inside a rig directory") } rigName := parts[0] // Load rig manager and get the rig rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) if err != nil { rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} } g := git.NewGit(townRoot) rigMgr := rig.NewManager(townRoot, rigsConfig, g) r, err := rigMgr.GetRig(rigName) if err != nil { return "", nil, fmt.Errorf("rig '%s' not found: %w", rigName, err) } return rigName, r, nil } func runMQRetry(cmd *cobra.Command, args []string) error { rigName := args[0] mrID := args[1] mgr, _, err := getRefineryManager(rigName) if err != nil { return err } // Get the MR first to show info mr, err := mgr.GetMR(mrID) if err != nil { if err == refinery.ErrMRNotFound { return fmt.Errorf("merge request '%s' not found in rig '%s'", mrID, rigName) } return fmt.Errorf("getting merge request: %w", err) } // Show what we're retrying fmt.Printf("Retrying merge request: %s\n", mrID) fmt.Printf(" Branch: %s\n", mr.Branch) fmt.Printf(" Worker: %s\n", mr.Worker) if mr.Error != "" { fmt.Printf(" Previous error: %s\n", style.Dim.Render(mr.Error)) } // Perform the retry if err := mgr.Retry(mrID, mqRetryNow); err != nil { if err == refinery.ErrMRNotFailed { return fmt.Errorf("merge request '%s' has not failed (status: %s)", mrID, mr.Status) } return fmt.Errorf("retrying merge request: %w", err) } if mqRetryNow { fmt.Printf("%s Merge request processed\n", style.Bold.Render("✓")) } else { fmt.Printf("%s Merge request queued for retry\n", style.Bold.Render("✓")) fmt.Printf(" %s\n", style.Dim.Render("Will be processed on next refinery cycle")) } return nil } func runMQReject(cmd *cobra.Command, args []string) error { rigName := args[0] mrIDOrBranch := args[1] mgr, _, err := getRefineryManager(rigName) if err != nil { return err } result, err := mgr.RejectMR(mrIDOrBranch, mqRejectReason, mqRejectNotify) if err != nil { return fmt.Errorf("rejecting MR: %w", err) } fmt.Printf("%s Rejected: %s\n", style.Bold.Render("✗"), result.Branch) fmt.Printf(" Worker: %s\n", result.Worker) fmt.Printf(" Reason: %s\n", mqRejectReason) if result.IssueID != "" { fmt.Printf(" Issue: %s %s\n", result.IssueID, style.Dim.Render("(not closed - work not done)")) } if mqRejectNotify { fmt.Printf(" %s\n", style.Dim.Render("Worker notified via mail")) } return nil }