diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 9c6e2c3a..30ff5023 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -436,8 +436,8 @@ func submitMRForPolecat() error { return nil } - // Run gt mq submit - submitCmd := exec.Command("gt", "mq", "submit") + // Run gt mq submit --no-cleanup (handoff manages lifecycle itself) + submitCmd := exec.Command("gt", "mq", "submit", "--no-cleanup") submitOutput, err := submitCmd.CombinedOutput() if err != nil { return fmt.Errorf("%s", strings.TrimSpace(string(submitOutput))) diff --git a/internal/cmd/mq.go b/internal/cmd/mq.go index 70279069..9471b31d 100644 --- a/internal/cmd/mq.go +++ b/internal/cmd/mq.go @@ -17,10 +17,11 @@ import ( // MQ command flags var ( // Submit flags - mqSubmitBranch string - mqSubmitIssue string - mqSubmitEpic string - mqSubmitPriority int + mqSubmitBranch string + mqSubmitIssue string + mqSubmitEpic string + mqSubmitPriority int + mqSubmitNoCleanup bool // Retry flags mqRetryNow bool @@ -62,7 +63,7 @@ var mqSubmitCmd = &cobra.Command{ 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 Engineer. +Creates a merge-request bead that will be processed by the Refinery. Auto-detection: - Branch: current git branch @@ -79,11 +80,20 @@ Target branch auto-detection: 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 + 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 --priority 0 # Override priority (P0) + gt mq submit --no-cleanup # Submit without auto-cleanup`, RunE: runMqSubmit, } @@ -243,6 +253,7 @@ func init() { 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") diff --git a/internal/cmd/mq_submit.go b/internal/cmd/mq_submit.go index fda746ae..68337c71 100644 --- a/internal/cmd/mq_submit.go +++ b/internal/cmd/mq_submit.go @@ -3,8 +3,10 @@ package cmd import ( "fmt" "os" + "os/exec" "regexp" "strings" + "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" @@ -165,6 +167,20 @@ func runMqSubmit(cmd *cobra.Command, args []string) error { } fmt.Printf(" Priority: P%d\n", priority) + // Auto-cleanup for polecats: if this is a polecat branch and cleanup not disabled, + // send lifecycle request and wait for termination + if worker != "" && !mqSubmitNoCleanup { + fmt.Println() + fmt.Printf("%s Auto-cleanup: polecat work submitted\n", style.Bold.Render("✓")) + if err := polecatCleanup(rigName, worker, townRoot); err != nil { + // Non-fatal: warn but return success (MR was created) + fmt.Printf("%s Could not auto-cleanup: %v\n", style.Warning.Render("Warning:"), err) + fmt.Println(style.Dim.Render(" You may need to run 'gt handoff --shutdown' manually")) + return nil + } + // polecatCleanup blocks forever waiting for termination, so we never reach here + } + return nil } @@ -217,3 +233,53 @@ func detectIntegrationBranch(bd *beads.Beads, g *git.Git, issueID string) (strin return "", nil // No integration branch found } + +// polecatCleanup sends a lifecycle shutdown request to the witness and waits for termination. +// This is called after a polecat successfully submits an MR. +func polecatCleanup(rigName, worker, townRoot string) error { + // Send lifecycle request to witness + manager := rigName + "/witness" + subject := fmt.Sprintf("LIFECYCLE: polecat-%s requesting shutdown", worker) + body := fmt.Sprintf(`Lifecycle request from polecat %s. + +Action: shutdown +Reason: MR submitted to merge queue +Time: %s + +Please verify state and execute lifecycle action. +`, worker, time.Now().Format(time.RFC3339)) + + // Send via gt mail + cmd := exec.Command("gt", "mail", "send", manager, + "-s", subject, + "-m", body, + ) + cmd.Dir = townRoot + + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("sending lifecycle request: %w: %s", err, string(out)) + } + fmt.Printf("%s Sent shutdown request to %s\n", style.Bold.Render("✓"), manager) + + // Wait for retirement with periodic status + fmt.Println() + fmt.Printf("%s Waiting for retirement...\n", style.Dim.Render("◌")) + fmt.Println(style.Dim.Render("(Witness will terminate this session)")) + + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + waitStart := time.Now() + for { + select { + case <-ticker.C: + elapsed := time.Since(waitStart).Round(time.Second) + fmt.Printf("%s Still waiting (%v elapsed)...\n", style.Dim.Render("◌"), elapsed) + if elapsed >= 2*time.Minute { + fmt.Println(style.Dim.Render(" Hint: If witness isn't responding, you may need to:")) + fmt.Println(style.Dim.Render(" - Check if witness is running")) + fmt.Println(style.Dim.Render(" - Use Ctrl+C to abort and manually exit")) + } + } + } +}