Commit 5abd6348ee41

Vincent Demeester <vincent@sbr.pm>
2025-12-12 15:38:47
feat(gh-pr): Add batch commenting and consolidate GitHub tools
- Enable commenting on multiple PRs with interactive fzf selection - Provide rich preview panes showing CI status and PR details - Consolidate gh-restart-failed and gh-resolve-conflicts into gh-pr Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 6fe1a0e
pkgs/default.nix
@@ -22,8 +22,6 @@ in
   batzconverter = pkgs.callPackage ./batzconverter { };
   manifest-tool = pkgs.callPackage ./manifest-tool { };
   gh-pr = pkgs.callPackage ../tools/gh-pr { };
-  gh-restart-failed = pkgs.callPackage ../tools/gh-restart-failed { };
-  gh-resolve-conflicts = pkgs.callPackage ../tools/gh-resolve-conflicts { };
   arr = pkgs.callPackage ../tools/arr { };
   download-kiwix-zim = pkgs.callPackage ../tools/download-kiwix-zim { };
   cliphist-cleanup = pkgs.callPackage ../tools/cliphist-cleanup { };
systems/kyushu/home.nix
@@ -56,8 +56,7 @@
     sbcl
 
     go-org-readwise
-    gh-restart-failed
-    gh-resolve-conflicts
+    gh-pr
     arr
     claude-hooks
     toggle-color-scheme
tools/gh-pr/cmd/gh-pr/comment.go
@@ -0,0 +1,233 @@
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/vdemeester/home/tools/gh-pr/internal/output"
+)
+
+func commentCmd(out *output.Writer) *cobra.Command {
+	var (
+		body   string
+		repo   string
+		labels []string
+		author string
+		state  string
+	)
+
+	cmd := &cobra.Command{
+		Use:   "comment",
+		Short: "Comment on multiple pull requests",
+		Long: `Select and comment on multiple pull requests using fzf.
+
+This command lists pull requests and allows you to select multiple PRs
+using fzf's multi-select feature. You can then add the same comment to
+all selected PRs.
+
+Examples:
+  gh-pr comment                           # Select PRs interactively
+  gh-pr comment --body "LGTM"            # Pre-specify the comment
+  gh-pr comment --label bug              # Filter by label
+  gh-pr comment --repo owner/repo        # Work with a specific repo
+  gh-pr comment --state all              # Include closed PRs`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runComment(out, commentOpts{
+				body:   body,
+				repo:   repo,
+				labels: labels,
+				author: author,
+				state:  state,
+			})
+		},
+	}
+
+	cmd.Flags().StringVarP(&body, "body", "b", "", "Comment body (will prompt if not provided)")
+	cmd.Flags().StringVarP(&repo, "repo", "R", "", "Repository (owner/repo format)")
+	cmd.Flags().StringSliceVarP(&labels, "label", "l", nil, "Filter PRs by label (comma-separated)")
+	cmd.Flags().StringVarP(&author, "author", "a", "", "Filter PRs by author")
+	cmd.Flags().StringVarP(&state, "state", "s", "open", "Filter by state: open, closed, merged, all")
+
+	return cmd
+}
+
+type commentOpts struct {
+	body   string
+	repo   string
+	labels []string
+	author string
+	state  string
+}
+
+func runComment(out *output.Writer, opts commentOpts) error {
+	// Check if fzf is available
+	if _, err := exec.LookPath("fzf"); err != nil {
+		return fmt.Errorf("fzf is required but not found in PATH: %w", err)
+	}
+
+	// Build gh pr list command
+	ghArgs := []string{"pr", "list"}
+
+	if opts.repo != "" {
+		ghArgs = append(ghArgs, "--repo", opts.repo)
+	}
+
+	for _, label := range opts.labels {
+		ghArgs = append(ghArgs, "--label", label)
+	}
+
+	if opts.author != "" {
+		ghArgs = append(ghArgs, "--author", opts.author)
+	}
+
+	if opts.state != "" {
+		ghArgs = append(ghArgs, "--state", opts.state)
+	}
+
+	// Get list of PRs
+	out.Info("Fetching pull requests...")
+	ghCmd := exec.Command("gh", ghArgs...)
+	ghOutput, err := ghCmd.Output()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); ok {
+			return fmt.Errorf("gh pr list failed: %s", exitErr.Stderr)
+		}
+		return fmt.Errorf("gh pr list failed: %w", err)
+	}
+
+	prList := strings.TrimSpace(string(ghOutput))
+	if prList == "" {
+		out.Warning("No pull requests found matching the criteria.")
+		return nil
+	}
+
+	// Build preview command for fzf
+	repoFlag := ""
+	if opts.repo != "" {
+		repoFlag = fmt.Sprintf("-R %s", opts.repo)
+	}
+
+	previewCmd := fmt.Sprintf(`gh pr view {1} %s --json number,title,author,statusCheckRollup | \
+		jq -r '"# PR " + (.number | tostring) + ": " + .title,
+		       "",
+		       "Author: @" + .author.login,
+		       "",
+		       "## Status Checks:",
+		       "",
+		       (if (.statusCheckRollup // [] | length) == 0 then "  (No status checks)"
+		        else (.statusCheckRollup | map(
+		         "  " + (
+		           if .conclusion == "SUCCESS" then "✓"
+		           elif .conclusion == "FAILURE" then "✗"
+		           elif .conclusion == "PENDING" then "●"
+		           elif .conclusion == "SKIPPED" then "○"
+		           else "?"
+		           end
+		         ) + " " + .name + " (" + (.conclusion // "unknown") + ")"
+		       ) | join("\n"))
+		        end)'`, repoFlag)
+
+	// Use fzf for multi-select with preview
+	out.Info("Select pull requests (use Tab to select multiple, Enter to confirm)...")
+	fzfCmd := exec.Command("fzf",
+		"--multi",
+		"--ansi",
+		"--header", "Select PRs (Tab: select, Enter: confirm)",
+		"--preview", previewCmd,
+		"--preview-window", "right:60%:wrap",
+	)
+	fzfCmd.Stdin = strings.NewReader(prList)
+	fzfCmd.Stderr = os.Stderr
+
+	selectedOutput, err := fzfCmd.Output()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 130 {
+			// User cancelled with Ctrl+C
+			out.Info("Selection cancelled.")
+			return nil
+		}
+		return fmt.Errorf("fzf selection failed: %w", err)
+	}
+
+	selectedPRs := strings.TrimSpace(string(selectedOutput))
+	if selectedPRs == "" {
+		out.Info("No pull requests selected.")
+		return nil
+	}
+
+	// Extract PR numbers from selected lines
+	prNumbers := []string{}
+	scanner := bufio.NewScanner(strings.NewReader(selectedPRs))
+	for scanner.Scan() {
+		line := scanner.Text()
+		// Extract PR number from the first field (format: "123  title...")
+		fields := strings.Fields(line)
+		if len(fields) > 0 {
+			prNumbers = append(prNumbers, fields[0])
+		}
+	}
+
+	if len(prNumbers) == 0 {
+		out.Warning("No valid PR numbers found in selection.")
+		return nil
+	}
+
+	out.Success("Selected %d pull request(s): %s", len(prNumbers), strings.Join(prNumbers, ", "))
+
+	// Get comment body if not provided
+	commentBody := opts.body
+	if commentBody == "" {
+		out.Info("Enter your comment (Ctrl+D when done):")
+		var buf bytes.Buffer
+		scanner := bufio.NewScanner(os.Stdin)
+		for scanner.Scan() {
+			buf.WriteString(scanner.Text())
+			buf.WriteString("\n")
+		}
+		if err := scanner.Err(); err != nil {
+			return fmt.Errorf("failed to read comment: %w", err)
+		}
+		commentBody = strings.TrimSpace(buf.String())
+	}
+
+	if commentBody == "" {
+		out.Warning("Empty comment body. No comments will be posted.")
+		return nil
+	}
+
+	// Comment on each PR
+	out.Info("Posting comment to %d pull request(s)...", len(prNumbers))
+	failed := []string{}
+
+	for _, prNum := range prNumbers {
+		commentArgs := []string{"pr", "comment", prNum, "--body", commentBody}
+		if opts.repo != "" {
+			commentArgs = append(commentArgs, "--repo", opts.repo)
+		}
+
+		commentCmd := exec.Command("gh", commentArgs...)
+		if err := commentCmd.Run(); err != nil {
+			out.Error("Failed to comment on PR #%s: %v", prNum, err)
+			failed = append(failed, prNum)
+		} else {
+			out.Success("Commented on PR #%s", prNum)
+		}
+	}
+
+	// Summary
+	successCount := len(prNumbers) - len(failed)
+	if successCount > 0 {
+		out.Success("\nSuccessfully commented on %d pull request(s)", successCount)
+	}
+	if len(failed) > 0 {
+		out.Error("Failed to comment on %d pull request(s): %s", len(failed), strings.Join(failed, ", "))
+		return fmt.Errorf("some comments failed")
+	}
+
+	return nil
+}
tools/gh-pr/cmd/gh-pr/main.go
@@ -36,6 +36,7 @@ and conflict resolution in a single command-line interface.`,
 	cmd.AddCommand(listTemplatesCmd(out))
 	cmd.AddCommand(restartFailedCmd(out))
 	cmd.AddCommand(resolveConflictsCmd(out))
+	cmd.AddCommand(commentCmd(out))
 
 	return cmd
 }
tools/gh-pr/cmd/gh-pr/resolve_conflicts.go
@@ -1,7 +1,10 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
+	"os"
+	"os/exec"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -112,31 +115,122 @@ func runResolveConflicts(out *output.Writer, opts resolveConflictsOpts) error {
 		return nil
 	}
 
-	out.Warning("Found %d pull request(s) with merge conflicts:", len(prs))
-	out.Println("")
+	out.Success("Found %d pull request(s) with merge conflicts", len(prs))
 
-	// Display PRs
-	for i, pr := range prs {
-		out.Println("%d. PR #%d: %s (@%s)", i+1, pr.Number, pr.Title, pr.Author.Login)
+	// Check if fzf is available
+	if _, err := exec.LookPath("fzf"); err != nil {
+		return fmt.Errorf("fzf is required but not found in PATH: %w", err)
 	}
 
-	out.Println("")
-	out.Info("Processing conflicting pull requests...")
+	// Build fzf input with formatted PR information
+	var fzfInput strings.Builder
+	prMap := make(map[int]*conflicts.PRInfo) // Map PR number to info for later lookup
+
+	for _, pr := range prs {
+		prMap[pr.Number] = &pr
+		// Format: "#123  Title here (@author) [repo]"
+		repo := pr.HeadRepository.NameWithOwner
+		fzfInput.WriteString(fmt.Sprintf("#%-6d %s (@%s) [%s]\n",
+			pr.Number, pr.Title, pr.Author.Login, repo))
+	}
+
+	// Build preview command for fzf
+	// We need to extract the repo from the selection and fetch PR details
+	previewCmd := `echo {} | awk '{print $1, $(NF)}' | sed 's/\[//;s/\]//' | \
+		xargs -I{} sh -c 'PR=$(echo {} | cut -d" " -f1); REPO=$(echo {} | cut -d" " -f2); \
+		gh pr view $PR -R $REPO --json number,title,author,headRefName,baseRefName,mergeable,statusCheckRollup 2>/dev/null | \
+		jq -r "\"# PR \" + (.number | tostring) + \": \" + .title,
+		       \"\",
+		       \"Author: @\" + .author.login,
+		       \"\",
+		       \"## Branches:\",
+		       \"\",
+		       \"  \" + .headRefName + \" → \" + .baseRefName,
+		       \"\",
+		       \"## Merge Status: \" + (.mergeable // \"unknown\"),
+		       \"\",
+		       \"## Status Checks:\",
+		       \"\",
+		       (if (.statusCheckRollup // [] | length) == 0 then \"  (No status checks)\"
+		        else (.statusCheckRollup | map(
+		         \"  \" + (
+		           if .conclusion == \"SUCCESS\" then \"✓\"
+		           elif .conclusion == \"FAILURE\" then \"✗\"
+		           elif .conclusion == \"PENDING\" then \"●\"
+		           elif .conclusion == \"SKIPPED\" then \"○\"
+		           else \"?\"
+		           end
+		         ) + \" \" + .name + \" (\" + (.conclusion // \"unknown\") + \")\"
+		       ) | join(\"\\n\"))
+		        end)"'`
+
+	// Use fzf for multi-select with preview
+	out.Info("Select pull requests to resolve (use Tab to select multiple, Enter to confirm)...")
+	fzfCmd := exec.Command("fzf",
+		"--multi",
+		"--ansi",
+		"--header", "Select PRs to resolve conflicts (Tab: select, Enter: confirm)",
+		"--preview", previewCmd,
+		"--preview-window", "right:60%:wrap",
+	)
+	fzfCmd.Stdin = strings.NewReader(fzfInput.String())
+	fzfCmd.Stderr = os.Stderr
+
+	selectedOutput, err := fzfCmd.Output()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 130 {
+			// User cancelled with Ctrl+C
+			out.Info("Selection cancelled.")
+			return nil
+		}
+		return fmt.Errorf("fzf selection failed: %w", err)
+	}
+
+	selectedPRs := strings.TrimSpace(string(selectedOutput))
+	if selectedPRs == "" {
+		out.Info("No pull requests selected.")
+		return nil
+	}
+
+	// Extract PR numbers from selected lines
+	selectedPRNumbers := []int{}
+	scanner := bufio.NewScanner(strings.NewReader(selectedPRs))
+	for scanner.Scan() {
+		line := scanner.Text()
+		// Extract PR number from format "#123  Title..."
+		if strings.HasPrefix(line, "#") {
+			var prNum int
+			if _, err := fmt.Sscanf(line, "#%d", &prNum); err == nil {
+				selectedPRNumbers = append(selectedPRNumbers, prNum)
+			}
+		}
+	}
+
+	if len(selectedPRNumbers) == 0 {
+		out.Warning("No valid PR numbers found in selection.")
+		return nil
+	}
+
+	out.Success("Selected %d pull request(s)", len(selectedPRNumbers))
 	out.Println("")
 
-	// Resolve each PR
-	for _, pr := range prs {
-		// Determine repository from PR
-		repo := fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
-		if pr.IsCrossRepository {
-			// For cross-repo PRs, we need the base repo
-			// This is a limitation - we'd need to track base repo in search results
-			out.Warning("Skipping cross-repository PR #%d (requires base repo info)", pr.Number)
+	// Resolve selected PRs
+	for _, prNum := range selectedPRNumbers {
+		pr, ok := prMap[prNum]
+		if !ok {
+			out.Warning("PR #%d not found in the list, skipping...", prNum)
 			continue
 		}
 
-		if err := resolver.ResolvePR(repo, &pr); err != nil {
-			out.Error("Failed to resolve PR #%d: %v", pr.Number, err)
+		// Determine repository from PR
+		repo := pr.HeadRepository.NameWithOwner
+		if pr.IsCrossRepository {
+			out.Warning("Skipping cross-repository PR #%d (requires manual handling)", prNum)
+			continue
+		}
+
+		if err := resolver.ResolvePR(repo, pr); err != nil {
+			out.Error("Failed to resolve PR #%d: %v", prNum, err)
 			out.Println("")
 			continue
 		}
tools/gh-pr/cmd/gh-pr/restart_failed.go
@@ -1,8 +1,10 @@
 package main
 
 import (
+	"bufio"
 	"encoding/json"
 	"fmt"
+	"os"
 	"os/exec"
 	"strings"
 
@@ -128,6 +130,11 @@ func restartSpecificPR(out *output.Writer, opts restartFailedOpts) error {
 }
 
 func restartInteractive(out *output.Writer, opts restartFailedOpts) error {
+	// Check if fzf is available
+	if _, err := exec.LookPath("fzf"); err != nil {
+		return fmt.Errorf("fzf is required but not found in PATH: %w", err)
+	}
+
 	out.Info("Fetching pull requests...")
 
 	// Build gh pr list command
@@ -152,18 +159,31 @@ func restartInteractive(out *output.Writer, opts restartFailedOpts) error {
 	}
 
 	// Filter PRs with failed checks
-	failedPRs := []prInfo{}
+	type failedPRInfo struct {
+		pr          prInfo
+		failedCount int
+		author      string
+	}
+
+	failedPRs := []failedPRInfo{}
 	for _, pr := range prs {
-		hasFailed := false
+		failedCount := 0
 		for _, check := range pr.StatusCheckRollup {
 			if check.Conclusion == "FAILURE" || check.Conclusion == "TIMED_OUT" ||
 				check.Conclusion == "STARTUP_FAILURE" || check.Conclusion == "ACTION_REQUIRED" {
-				hasFailed = true
-				break
+				failedCount++
 			}
 		}
-		if hasFailed {
-			failedPRs = append(failedPRs, pr)
+		if failedCount > 0 {
+			author := "unknown"
+			if login, ok := pr.Author["login"].(string); ok {
+				author = login
+			}
+			failedPRs = append(failedPRs, failedPRInfo{
+				pr:          pr,
+				failedCount: failedCount,
+				author:      author,
+			})
 		}
 	}
 
@@ -172,35 +192,103 @@ func restartInteractive(out *output.Writer, opts restartFailedOpts) error {
 		return nil
 	}
 
-	out.Warning("Found %d pull request(s) with failed checks:", len(failedPRs))
-	out.Println("")
+	out.Success("Found %d pull request(s) with failed checks", len(failedPRs))
 
-	// Display PRs for user
-	for i, pr := range failedPRs {
-		failedCount := 0
-		for _, check := range pr.StatusCheckRollup {
-			if check.Conclusion == "FAILURE" || check.Conclusion == "TIMED_OUT" ||
-				check.Conclusion == "STARTUP_FAILURE" || check.Conclusion == "ACTION_REQUIRED" {
-				failedCount++
-			}
-		}
+	// Build fzf input with formatted PR information
+	var fzfInput strings.Builder
+	prMap := make(map[int]failedPRInfo) // Map PR number to info for later lookup
 
-		author := "unknown"
-		if login, ok := pr.Author["login"].(string); ok {
-			author = login
-		}
-
-		out.Println("%d. PR #%d: %s (@%s) - %d failed", i+1, pr.Number, pr.Title, author, failedCount)
+	for _, fpr := range failedPRs {
+		prMap[fpr.pr.Number] = fpr
+		// Format: "#123  Title here (@author) - 2 failed"
+		fzfInput.WriteString(fmt.Sprintf("#%-6d %s (@%s) - %d failed\n",
+			fpr.pr.Number, fpr.pr.Title, fpr.author, fpr.failedCount))
 	}
 
-	out.Println("")
-	out.Info("Processing all PRs with failed workflows...")
+	// Build preview command for fzf
+	repoFlag := ""
+	if opts.repo != "" {
+		repoFlag = fmt.Sprintf("-R %s", opts.repo)
+	}
+
+	previewCmd := fmt.Sprintf(`gh pr view {1} %s --json number,title,author,statusCheckRollup | \
+		jq -r '"# PR " + (.number | tostring) + ": " + .title,
+		       "",
+		       "Author: @" + .author.login,
+		       "",
+		       "## Status Checks:",
+		       "",
+		       (.statusCheckRollup // [] | map(
+		         "  " + (
+		           if .conclusion == "SUCCESS" then "✓"
+		           elif .conclusion == "FAILURE" then "✗"
+		           elif .conclusion == "PENDING" then "●"
+		           elif .conclusion == "SKIPPED" then "○"
+		           else "?"
+		           end
+		         ) + " " + .name + " (" + (.conclusion // "unknown") + ")"
+		       ) | join("\n"))'`, repoFlag)
+
+	// Use fzf for multi-select with preview
+	out.Info("Select pull requests to restart (use Tab to select multiple, Enter to confirm)...")
+	fzfCmd := exec.Command("fzf",
+		"--multi",
+		"--ansi",
+		"--header", "Select PRs to restart workflows (Tab: select, Enter: confirm)",
+		"--preview", previewCmd,
+		"--preview-window", "right:60%:wrap",
+	)
+	fzfCmd.Stdin = strings.NewReader(fzfInput.String())
+	fzfCmd.Stderr = os.Stderr
+
+	selectedOutput, err := fzfCmd.Output()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 130 {
+			// User cancelled with Ctrl+C
+			out.Info("Selection cancelled.")
+			return nil
+		}
+		return fmt.Errorf("fzf selection failed: %w", err)
+	}
+
+	selectedPRs := strings.TrimSpace(string(selectedOutput))
+	if selectedPRs == "" {
+		out.Info("No pull requests selected.")
+		return nil
+	}
+
+	// Extract PR numbers from selected lines
+	selectedPRNumbers := []int{}
+	scanner := bufio.NewScanner(strings.NewReader(selectedPRs))
+	for scanner.Scan() {
+		line := scanner.Text()
+		// Extract PR number from format "#123  Title..."
+		if strings.HasPrefix(line, "#") {
+			var prNum int
+			if _, err := fmt.Sscanf(line, "#%d", &prNum); err == nil {
+				selectedPRNumbers = append(selectedPRNumbers, prNum)
+			}
+		}
+	}
+
+	if len(selectedPRNumbers) == 0 {
+		out.Warning("No valid PR numbers found in selection.")
+		return nil
+	}
+
+	out.Success("Selected %d pull request(s)", len(selectedPRNumbers))
 	out.Println("")
 
-	// Restart workflows for each PR
-	for _, pr := range failedPRs {
-		out.Info("PR #%d: %s", pr.Number, pr.Title)
-		if err := restartPRWorkflows(out, opts, pr.Number, pr.HeadRefName); err != nil {
+	// Restart workflows for selected PRs
+	for _, prNum := range selectedPRNumbers {
+		fpr, ok := prMap[prNum]
+		if !ok {
+			out.Warning("PR #%d not found in the list, skipping...", prNum)
+			continue
+		}
+
+		out.Info("PR #%d: %s", fpr.pr.Number, fpr.pr.Title)
+		if err := restartPRWorkflows(out, opts, fpr.pr.Number, fpr.pr.HeadRefName); err != nil {
 			out.Error("Failed to restart workflows: %v", err)
 		}
 		out.Println("")
tools/gh-pr/default.nix
@@ -2,7 +2,10 @@
   buildGoModule,
   lib,
   makeWrapper,
+  installShellFiles,
   gh,
+  fzf,
+  jq,
 }:
 
 buildGoModule {
@@ -12,19 +15,34 @@ buildGoModule {
 
   vendorHash = "sha256-hocnLCzWN8srQcO3BMNkd2lt0m54Qe7sqAhUxVZlz1k="; # No external dependencies
 
-  nativeBuildInputs = [ makeWrapper ];
+  nativeBuildInputs = [
+    makeWrapper
+    installShellFiles
+  ];
 
   # Build all binaries
   subPackages = [ "cmd/gh-pr" ];
 
-  # Wrap binary to include gh in PATH
+  # Wrap binary to include gh, fzf, and jq in PATH and install completions
   postInstall = ''
     wrapProgram $out/bin/gh-pr \
-      --prefix PATH : ${lib.makeBinPath [ gh ]}
+      --prefix PATH : ${
+        lib.makeBinPath [
+          gh
+          fzf
+          jq
+        ]
+      }
+
+    # Generate shell completions
+    installShellCompletion --cmd gh-pr \
+      --bash <($out/bin/gh-pr completion bash) \
+      --fish <($out/bin/gh-pr completion fish) \
+      --zsh <($out/bin/gh-pr completion zsh)
   '';
 
   meta = {
-    description = "GitHub Pull Request management tool with template support, workflow restart, and conflict resolution";
+    description = "GitHub Pull Request management tool with template support, workflow restart, conflict resolution, and batch commenting";
     license = lib.licenses.mit;
     platforms = lib.platforms.unix;
     mainProgram = "gh-pr";
tools/gh-pr/README.md
@@ -7,6 +7,7 @@ A comprehensive GitHub Pull Request management tool written in Go, consolidating
 - **PR Creation with Templates**: Create pull requests with automatic template discovery and caching
 - **Template Management**: List and preview templates from local or remote repositories
 - **Remote Template Discovery**: Browse templates from any GitHub repository
+- **Batch Commenting**: Comment on multiple pull requests at once using fzf multi-select
 - **Workflow Restart**: Automatically restart failed GitHub Actions workflows
 - **Conflict Resolution**: Interactive merge conflict resolution with worktree support
 - **Template Caching**: Templates are cached for one week to speed up operations
@@ -72,6 +73,53 @@ Templates are automatically discovered from:
 - `.github/PULL_REQUEST_TEMPLATE/`
 - `docs/PULL_REQUEST_TEMPLATE.md`
 
+### `gh-pr comment`
+
+Comment on multiple pull requests at once using interactive selection.
+
+```bash
+# Interactive mode - select PRs and enter comment
+gh-pr comment
+
+# Pre-specify the comment body
+gh-pr comment --body "LGTM! Approving this change."
+
+# Filter by label before selecting
+gh-pr comment --label bug --label urgent
+
+# Work with a specific repository
+gh-pr comment --repo owner/repo
+
+# Include closed PRs in the selection
+gh-pr comment --state all
+
+# Filter by author
+gh-pr comment --author username
+```
+
+**Options:**
+- `-b, --body`: Comment body (will prompt if not provided)
+- `-R, --repo`: Repository in "owner/repo" format
+- `-l, --label`: Filter PRs by label (can be used multiple times)
+- `-a, --author`: Filter PRs by author
+- `-s, --state`: Filter by state: open, closed, merged, all (default: open)
+
+**How It Works:**
+
+1. **List PRs**: Fetches pull requests matching your filters using `gh pr list`
+2. **Select**: Uses fzf for multi-select with preview pane showing:
+   - PR title and author
+   - CI check status with visual indicators (✓/✗/●/○)
+   - Tab to select, Enter to confirm
+3. **Comment**: Prompts for comment body if not provided via `--body`
+4. **Post**: Posts the same comment to all selected PRs
+
+**Use Cases:**
+- Notify multiple PRs about a related change
+- Request updates across multiple related PRs
+- Add acknowledgments to a batch of PRs
+- Communicate breaking changes to affected PRs
+
 ### `gh-pr list-templates`
 
 List all available PR templates in the current or a remote repository.
@@ -113,16 +161,16 @@ This is especially useful for:
 
 ### `gh-pr restart-failed`
 
-Restart failed workflow runs on pull requests.
+Restart failed workflow runs on pull requests with interactive selection.
 
 ```bash
-# Interactive mode - list all PRs with failed checks
+# Interactive mode - select PRs with failed checks using fzf
 gh-pr restart-failed
 
-# Restart workflows for a specific PR
+# Restart workflows for a specific PR (no selection needed)
 gh-pr restart-failed owner/repo#123
 
-# Filter by label
+# Filter by label before selecting
 gh-pr restart-failed --label bug
 
 # Ignore specific workflows
@@ -136,6 +184,15 @@ gh-pr restart-failed owner/repo
 - `-i, --ignore`: Ignore workflows matching pattern (can be used multiple times)
 - `-l, --label`: Filter PRs by label (can be used multiple times)
 
+**How It Works:**
+
+1. **Find Failed PRs**: Fetches all PRs and filters those with failed checks
+2. **Select**: Uses fzf for multi-select with preview pane showing:
+   - All CI check statuses with visual indicators (✓/✗/●/○)
+   - Author information
+   - Tab to select, Enter to confirm
+3. **Restart**: Restarts failed workflows for all selected PRs
+
 **Default Behavior:**
 - "Label Checker" workflows are ignored by default
 - Only restarts workflows that failed due to:
@@ -143,19 +200,20 @@ gh-pr restart-failed owner/repo
   - `timed_out`
   - `startup_failure`
   - `action_required`
+- Shows failed count for each PR in the selection interface
 
 ### `gh-pr resolve-conflicts`
 
-Resolve merge conflicts in pull requests interactively with full worktree support.
+Resolve merge conflicts in pull requests interactively with full worktree support and fzf selection.
 
 ```bash
-# Search for your conflicting PRs
+# Search for your conflicting PRs and select with fzf
 gh-pr resolve-conflicts
 
-# Resolve a specific PR
+# Resolve a specific PR (no selection)
 gh-pr resolve-conflicts owner/repo#123
 
-# Filter by organization
+# Filter by organization before selecting
 gh-pr resolve-conflicts -o tektoncd
 
 # Use existing repo instead of creating worktree
@@ -178,13 +236,18 @@ gh-pr resolve-conflicts -w /tmp/my-worktrees
 **How It Works:**
 
 1. **Find Conflicting PRs**: Searches for open PRs with merge conflicts
-2. **Setup Worktree**: Creates an isolated worktree for each PR (or uses existing repo)
-3. **Fetch Branches**: Fetches both the PR branch and upstream base branch
-4. **Rebase**: Attempts to rebase the PR onto the base branch
-5. **Resolve Conflicts**: Launches conflict resolution tool:
+2. **Select**: Uses fzf for multi-select with preview pane showing:
+   - Branch information (head → base)
+   - Merge status
+   - CI check status with visual indicators (✓/✗/●/○)
+   - Tab to select, Enter to confirm
+3. **Setup Worktree**: Creates an isolated worktree for each PR (or uses existing repo)
+4. **Fetch Branches**: Fetches both the PR branch and upstream base branch
+5. **Rebase**: Attempts to rebase the PR onto the base branch
+6. **Resolve Conflicts**: Launches conflict resolution tool:
    - Tries `emacs` with ediff mode first
    - Falls back to `git mergetool` if emacs is unavailable
-6. **Force Push**: Optionally force-pushes the resolved changes
+7. **Force Push**: Optionally force-pushes the resolved changes
 
 **Fork Support:**
 
@@ -255,15 +318,31 @@ gh-pr create \
   --reviewer team-lead
 ```
 
+### Commenting on Multiple PRs
+
+```bash
+# Select and comment on multiple PRs interactively
+gh-pr comment
+
+# Comment on all PRs with a specific label
+gh-pr comment --label needs-rebase --body "Please rebase on main"
+
+# Notify all your open PRs about a breaking change
+gh-pr comment --author @me --body "Note: This depends on #1234"
+```
+
 ### Restarting Failed Workflows
 
 ```bash
-# See all PRs with failures and restart them
+# Select PRs with failures and restart workflows
 gh-pr restart-failed
 
-# Restart specific PR in another repo
+# Restart specific PR in another repo (no selection)
 gh-pr restart-failed tektoncd/pipeline#1234
 
+# Filter and select PRs with specific label
+gh-pr restart-failed --label bug
+
 # Ignore flaky tests
 gh-pr restart-failed --ignore "e2e-tests"
 ```
@@ -296,7 +375,8 @@ nix build .#gh-pr
 ## Dependencies
 
 - `gh` (GitHub CLI) - Required for all GitHub operations
-- `jq` - Used for JSON parsing in workflow operations
+- `fzf` - Required for interactive PR selection with preview
+- `jq` - Required for formatting preview pane and JSON parsing
 - Go 1.23+ - For building from source
 
 ## License
@@ -307,9 +387,9 @@ MIT
 
 - [x] Full implementation of `resolve-conflicts` command
 - [x] Remote repository template discovery
+- [x] Batch operations on multiple PRs (comment command)
 - [ ] Interactive PR selection with `fzf` integration for conflict resolution
 - [ ] Support for PR templates in multiple formats (YAML, JSON)
-- [ ] Batch operations on multiple PRs
 - [ ] Custom cache TTL configuration
 - [ ] Integration with review tools
 - [ ] Template validation and linting
tools/gh-resolve-conflicts/default.nix
@@ -1,42 +0,0 @@
-{
-  stdenv,
-  lib,
-  makeWrapper,
-  gh,
-  fzf,
-  jq,
-  git,
-  emacs,
-}:
-
-stdenv.mkDerivation {
-  name = "gh-resolve-conflicts";
-  pname = "gh-resolve-conflicts";
-  version = "0.1.0";
-
-  src = ./.;
-
-  nativeBuildInputs = [ makeWrapper ];
-
-  installPhase = ''
-    mkdir -p $out/bin
-    cp gh-resolve-conflicts.sh $out/bin/gh-resolve-conflicts
-    chmod +x $out/bin/gh-resolve-conflicts
-
-    wrapProgram $out/bin/gh-resolve-conflicts \
-      --prefix PATH : ${
-        lib.makeBinPath [
-          gh
-          fzf
-          jq
-          git
-          emacs
-        ]
-      }
-  '';
-
-  meta = {
-    description = "List and resolve merge conflicts in GitHub pull requests interactively";
-    platforms = lib.platforms.unix;
-  };
-}
tools/gh-resolve-conflicts/gh-resolve-conflicts.sh
@@ -1,623 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
-
-# Help message
-usage() {
-    cat <<EOF
-Usage: gh-resolve-conflicts [OPTIONS] [REPOSITORY[#PR_NUMBER]]
-
-List pull requests with merge conflicts and resolve them interactively.
-
-Options:
-    -w, --worktree DIR     Create worktrees in DIR (default: /tmp/gh-resolve-conflicts-worktrees)
-    -n, --no-worktree      Use existing repo (cd into it) instead of creating worktrees
-    -N, --no-push          Do NOT automatically force-push after resolution (default: auto-push)
-    -o, --org ORG          Filter PRs by organization
-    -a, --author AUTHOR    Filter PRs by author (default: @me)
-    -h, --help             Show this help message
-
-Arguments:
-    REPOSITORY    Optional repository in OWNER/REPO format.
-                  If not provided, searches across all repos (when -o is used).
-                  Can include #PR_NUMBER to directly resolve a specific PR.
-
-Dependencies:
-    - gh (GitHub CLI)
-    - fzf (fuzzy finder, for interactive mode)
-    - jq (JSON processor)
-    - git
-    - emacs (for ediff conflict resolution)
-
-Examples:
-    gh-resolve-conflicts                                    # List all your conflicting PRs (interactive)
-    gh-resolve-conflicts -o tektoncd                        # List conflicting PRs in tektoncd org
-    gh-resolve-conflicts owner/repo#123                     # Directly resolve PR #123
-    gh-resolve-conflicts -N                                 # Don't auto-push after resolution
-    gh-resolve-conflicts -n                                 # Use existing repo, no worktree
-
-Workflow:
-    1. Scans for PRs with merge conflicts
-    2. Interactive selection with fzf (multi-select supported)
-    3. For each PR:
-       - Creates worktree or uses existing repo
-       - Checks out PR branch
-       - Attempts rebase against base branch
-       - Launches emacs ediff for conflicts
-       - Continues rebase after resolution
-       - Optionally force-pushes
-
-EOF
-    exit 0
-}
-
-# Check dependencies
-check_dependencies() {
-    local missing=()
-
-    for cmd in gh jq git; do
-        if ! command -v "$cmd" &> /dev/null; then
-            missing+=("$cmd")
-        fi
-    done
-
-    if [ ${#missing[@]} -gt 0 ]; then
-        echo -e "${RED}Error: Missing required dependencies: ${missing[*]}${NC}" >&2
-        echo "Please install them and try again." >&2
-        exit 1
-    fi
-}
-
-# Check if emacs is available
-check_emacs() {
-    if ! command -v emacs &> /dev/null; then
-        echo -e "${YELLOW}Warning: emacs not found. Conflict resolution will use default git merge tool.${NC}" >&2
-        return 1
-    fi
-    return 0
-}
-
-# Default settings
-WORKTREE_DIR="/tmp/gh-resolve-conflicts-worktrees"
-USE_WORKTREE=true
-AUTO_PUSH=true
-ORG_FILTER=""
-AUTHOR_FILTER="@me"
-REPO_ARG=""
-PR_NUMBER=""
-
-# Parse arguments
-while [[ $# -gt 0 ]]; do
-    case $1 in
-        -h|--help)
-            usage
-            ;;
-        -w|--worktree)
-            if [ -n "${2:-}" ]; then
-                WORKTREE_DIR="$2"
-                shift 2
-            else
-                echo -e "${RED}Error: --worktree requires a directory argument${NC}" >&2
-                exit 1
-            fi
-            ;;
-        -n|--no-worktree)
-            USE_WORKTREE=false
-            shift
-            ;;
-        -N|--no-push)
-            AUTO_PUSH=false
-            shift
-            ;;
-        -o|--org)
-            if [ -n "${2:-}" ]; then
-                ORG_FILTER="$2"
-                shift 2
-            else
-                echo -e "${RED}Error: --org requires an organization argument${NC}" >&2
-                exit 1
-            fi
-            ;;
-        -a|--author)
-            if [ -n "${2:-}" ]; then
-                AUTHOR_FILTER="$2"
-                shift 2
-            else
-                echo -e "${RED}Error: --author requires an author argument${NC}" >&2
-                exit 1
-            fi
-            ;;
-        -*)
-            echo -e "${RED}Error: Unknown option: $1${NC}" >&2
-            usage
-            ;;
-        *)
-            REPO_ARG="$1"
-            # Check if it contains #PR_NUMBER
-            if [[ "$REPO_ARG" =~ ^(.+)#([0-9]+)$ ]]; then
-                REPO_ARG="${BASH_REMATCH[1]}"
-                PR_NUMBER="${BASH_REMATCH[2]}"
-            fi
-            shift
-            ;;
-    esac
-done
-
-check_dependencies
-
-# Check fzf only if in interactive mode (no PR_NUMBER specified)
-if [ -z "$PR_NUMBER" ] && ! command -v fzf &> /dev/null; then
-    echo -e "${RED}Error: fzf is required for interactive mode${NC}" >&2
-    echo "Please install fzf or specify a PR number directly (e.g., owner/repo#123)" >&2
-    exit 1
-fi
-
-HAS_EMACS=false
-if check_emacs; then
-    HAS_EMACS=true
-fi
-
-# Fetch conflicting PRs
-echo -e "${BLUE}Fetching pull requests with merge conflicts...${NC}" >&2
-
-# Build search query
-SEARCH_ARGS=(--author "$AUTHOR_FILTER" --state open)
-if [ -n "$ORG_FILTER" ]; then
-    SEARCH_ARGS+=(--owner "$ORG_FILTER")
-fi
-
-if [ -n "$PR_NUMBER" ]; then
-    # Direct PR mode
-    if [ -z "$REPO_ARG" ]; then
-        echo -e "${RED}Error: Repository must be specified when using #PR_NUMBER${NC}" >&2
-        exit 1
-    fi
-
-    echo -e "${BLUE}Fetching PR #$PR_NUMBER from $REPO_ARG...${NC}" >&2
-
-    pr_info=$(gh pr view "$PR_NUMBER" -R "$REPO_ARG" \
-        --json number,title,headRefName,baseRefName,author,mergeable,url \
-        2>/dev/null)
-
-    if [ -z "$pr_info" ]; then
-        echo -e "${RED}Error: PR #$PR_NUMBER not found${NC}" >&2
-        exit 1
-    fi
-
-    mergeable=$(echo "$pr_info" | jq -r '.mergeable')
-    if [ "$mergeable" != "CONFLICTING" ]; then
-        echo -e "${YELLOW}PR #$PR_NUMBER does not have merge conflicts (status: $mergeable)${NC}"
-        exit 0
-    fi
-
-    pr_title=$(echo "$pr_info" | jq -r '.title')
-    pr_branch=$(echo "$pr_info" | jq -r '.headRefName')
-    base_branch=$(echo "$pr_info" | jq -r '.baseRefName')
-    pr_author=$(echo "$pr_info" | jq -r '.author.login')
-    pr_url=$(echo "$pr_info" | jq -r '.url')
-
-    selected_prs="$REPO_ARG|#$PR_NUMBER|$pr_title|@$pr_author|$pr_branch|$base_branch|$pr_url"
-else
-    # Interactive mode: Search for conflicting PRs
-    prs_json=$(gh search prs "${SEARCH_ARGS[@]}" \
-        --json number,title,repository,url \
-        --limit 100)
-
-    if [ -z "$prs_json" ] || [ "$prs_json" = "[]" ]; then
-        echo -e "${YELLOW}No open pull requests found.${NC}"
-        exit 0
-    fi
-
-    # Check each PR for conflicts
-    conflicting_prs=""
-    total=$(echo "$prs_json" | jq 'length')
-    current=0
-
-    echo -e "${BLUE}Checking $total PRs for merge conflicts...${NC}" >&2
-
-    while IFS= read -r pr; do
-        ((current++)) || true
-        repo=$(echo "$pr" | jq -r '.repository.nameWithOwner')
-        number=$(echo "$pr" | jq -r '.number')
-        title=$(echo "$pr" | jq -r '.title')
-        url=$(echo "$pr" | jq -r '.url')
-
-        echo -ne "${YELLOW}\rChecking PR $current/$total...${NC}" >&2
-
-        # Fetch detailed PR info including mergeable status
-        pr_details=$(gh pr view "$number" -R "$repo" \
-            --json mergeable,headRefName,baseRefName,author 2>/dev/null || echo "{}")
-
-        mergeable=$(echo "$pr_details" | jq -r '.mergeable // "UNKNOWN"')
-
-        if [ "$mergeable" = "CONFLICTING" ]; then
-            branch=$(echo "$pr_details" | jq -r '.headRefName')
-            base_branch=$(echo "$pr_details" | jq -r '.baseRefName')
-            author=$(echo "$pr_details" | jq -r '.author.login')
-            conflicting_prs+="$repo|#$number|$title|@$author|$branch|$base_branch|$url"$'\n'
-        fi
-    done < <(echo "$prs_json" | jq -c '.[]')
-
-    echo -e "\r${GREEN}Done checking PRs.${NC}                    " >&2
-
-    if [ -z "$conflicting_prs" ]; then
-        echo -e "${GREEN}No pull requests with merge conflicts found!${NC}"
-        exit 0
-    fi
-
-    echo -e "${YELLOW}Found pull requests with merge conflicts:${NC}" >&2
-    echo ""
-
-    # Use fzf to select PRs
-    selected_prs=$(echo "$conflicting_prs" | fzf \
-        --multi \
-        --ansi \
-        --delimiter='|' \
-        --with-nth=1,2,3,4 \
-        --header="Select PRs to resolve conflicts (TAB for multi-select, ENTER to confirm)" \
-        --preview="echo {} | cut -d'|' -f7 | xargs -I % echo 'URL: %'; echo ''; repo=\$(echo {} | cut -d'|' -f1); pr=\$(echo {} | cut -d'|' -f2 | tr -d '#'); gh pr diff \"\$pr\" -R \"\$repo\" 2>/dev/null | head -50 || echo 'Loading...'" \
-        --preview-window=right:60%:wrap \
-        --bind='ctrl-/:toggle-preview' \
-        --height=80%)
-
-    if [ -z "$selected_prs" ]; then
-        echo -e "${YELLOW}No pull requests selected.${NC}"
-        exit 0
-    fi
-fi
-
-echo ""
-echo -e "${BLUE}Processing selected pull requests...${NC}"
-echo ""
-
-# Function to resolve conflicts with emacs ediff
-resolve_with_ediff() {
-    local file="$1"
-
-    if [ "$HAS_EMACS" = false ]; then
-        echo -e "${YELLOW}Emacs not available, using git mergetool...${NC}"
-        git mergetool "$file"
-        return $?
-    fi
-
-    echo -e "${GREEN}Launching emacs ediff for: $file${NC}"
-
-    # Create temporary elisp script for ediff
-    local ediff_script
-    ediff_script=$(mktemp)
-    cat > "$ediff_script" <<'ELISP'
-(defun resolve-conflict-and-quit ()
-  "Resolve git conflict with ediff and quit when done."
-  (let* ((file (car command-line-args-left))
-         (buffer (find-file-noselect file)))
-    (with-current-buffer buffer
-      ;; Check if file has conflict markers
-      (goto-char (point-min))
-      (if (search-forward "<<<<<<< " nil t)
-          (progn
-            ;; Use ediff-merge for 3-way merge
-            (let* ((base-file (concat file ".base"))
-                   (local-file (concat file ".LOCAL"))
-                   (remote-file (concat file ".REMOTE")))
-              (if (and (file-exists-p local-file)
-                       (file-exists-p remote-file))
-                  ;; If git created the temp files, use them
-                  (ediff-merge-files-with-ancestor local-file remote-file base-file nil file)
-                ;; Otherwise, try to extract from conflict markers
-                (vc-resolve-conflicts))))
-        ;; No conflict markers found
-        (message "No conflict markers found in %s" file)))
-    (setq command-line-args-left nil)))
-
-(add-hook 'ediff-quit-hook
-          (lambda ()
-            (save-buffers-kill-terminal t)))
-
-(resolve-conflict-and-quit)
-ELISP
-
-    # Launch emacs with ediff (using user's config)
-    emacs --load "$ediff_script" "$file" 2>/dev/null
-
-    rm -f "$ediff_script"
-
-    # Check if conflict markers still exist
-    if grep -q "<<<<<<< " "$file"; then
-        echo -e "${RED}Conflict markers still present in $file${NC}"
-        return 1
-    fi
-
-    # Mark as resolved
-    git add "$file"
-    return 0
-}
-
-# Function to resolve a single PR
-resolve_pr() {
-    local repo="$1"
-    local pr_number="$2"
-    local pr_title="$3"
-    local pr_branch="$4"
-    local base_branch="$5"
-    local pr_url="$6"
-
-    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
-    echo -e "${BLUE}Repository: $repo${NC}"
-    echo -e "${BLUE}PR #$pr_number: $pr_title${NC}"
-    echo -e "${BLUE}Branch: $pr_branch -> $base_branch${NC}"
-    echo -e "${BLUE}URL: $pr_url${NC}"
-    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
-    echo ""
-
-    # Get PR details to find the head repository (fork)
-    echo -e "${YELLOW}Finding fork repository...${NC}"
-    pr_details=$(gh pr view "$pr_number" -R "$repo" \
-        --json headRepository,headRepositoryOwner,isCrossRepository 2>/dev/null)
-
-    if [ -z "$pr_details" ]; then
-        echo -e "${RED}Failed to get PR details${NC}"
-        return 1
-    fi
-
-    local is_cross_repo
-    is_cross_repo=$(echo "$pr_details" | jq -r '.isCrossRepository')
-    local fork_repo
-
-    if [ "$is_cross_repo" = "true" ]; then
-        # PR is from a fork - construct fork repo name
-        local fork_owner
-        fork_owner=$(echo "$pr_details" | jq -r '.headRepositoryOwner.login')
-        local fork_name
-        fork_name=$(echo "$pr_details" | jq -r '.headRepository.name')
-
-        # Try to get full nameWithOwner, fall back to constructing it
-        fork_repo=$(echo "$pr_details" | jq -r '.headRepository.nameWithOwner')
-        if [ -z "$fork_repo" ] || [ "$fork_repo" = "null" ] || [ "$fork_repo" = "" ]; then
-            fork_repo="$fork_owner/$fork_name"
-        fi
-
-        echo -e "${BLUE}PR is from fork: $fork_repo${NC}"
-    else
-        # PR is from same repo (branch)
-        fork_repo="$repo"
-        echo -e "${BLUE}PR is from branch in same repo${NC}"
-    fi
-
-    local work_dir
-
-    if [ "$USE_WORKTREE" = true ]; then
-        # Use worktree
-        local repo_name
-        repo_name=$(echo "$repo" | tr '/' '-')
-        work_dir="$WORKTREE_DIR/$repo_name/pr-$pr_number"
-
-        echo -e "${YELLOW}Creating worktree at: $work_dir${NC}"
-
-        # Create parent directory
-        mkdir -p "$WORKTREE_DIR/$repo_name"
-
-        # Clone fork if not exists
-        local repo_dir="$WORKTREE_DIR/$repo_name/main"
-        if [ ! -d "$repo_dir" ]; then
-            echo -e "${YELLOW}Cloning fork: $fork_repo...${NC}"
-            gh repo clone "$fork_repo" "$repo_dir" -- --bare
-
-            # Add upstream remote if this is a fork
-            if [ "$is_cross_repo" = "true" ]; then
-                echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
-                git -C "$repo_dir" remote add upstream "https://github.com/$repo.git" 2>/dev/null || true
-            fi
-        else
-            echo -e "${YELLOW}Fetching latest changes from fork...${NC}"
-            git -C "$repo_dir" fetch origin
-
-            # Ensure upstream remote exists if this is a fork
-            if [ "$is_cross_repo" = "true" ]; then
-                if ! git -C "$repo_dir" remote | grep -q "^upstream$"; then
-                    echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
-                    git -C "$repo_dir" remote add upstream "https://github.com/$repo.git"
-                fi
-            fi
-        fi
-
-        # Fetch from upstream if this is a fork
-        if [ "$is_cross_repo" = "true" ]; then
-            echo -e "${YELLOW}Fetching from upstream: $repo...${NC}"
-            git -C "$repo_dir" fetch upstream
-        fi
-
-        # Remove existing worktree if present
-        if [ -d "$work_dir" ]; then
-            echo -e "${YELLOW}Removing existing worktree...${NC}"
-            git -C "$repo_dir" worktree remove "$work_dir" --force 2>/dev/null || rm -rf "$work_dir"
-        fi
-
-        # Fetch PR branch from fork
-        echo -e "${YELLOW}Fetching PR branch: $pr_branch...${NC}"
-        git -C "$repo_dir" fetch origin "$pr_branch:pr-$pr_number" || {
-            echo -e "${RED}Failed to fetch PR branch from fork${NC}"
-            return 1
-        }
-
-        # Create new worktree
-        echo -e "${YELLOW}Creating worktree for branch $pr_branch...${NC}"
-        git -C "$repo_dir" worktree add "$work_dir" "pr-$pr_number" || {
-            echo -e "${RED}Failed to create worktree${NC}"
-            return 1
-        }
-
-        cd "$work_dir"
-    else
-        # Use existing repo
-        echo -e "${YELLOW}Using existing repository (no worktree)${NC}"
-
-        # Determine which repo we should be in
-        current_repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
-
-        # We should be in the fork, not upstream
-        if [ "$current_repo" != "$fork_repo" ]; then
-            echo -e "${RED}Error: Current directory is $current_repo, expected $fork_repo${NC}"
-            echo -e "${YELLOW}Please cd to your fork or use --worktree mode${NC}"
-            return 1
-        fi
-
-        work_dir=$(pwd)
-
-        # Ensure upstream remote exists if this is a fork
-        if [ "$is_cross_repo" = "true" ]; then
-            if ! git remote | grep -q "^upstream$"; then
-                echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
-                git remote add upstream "https://github.com/$repo.git"
-            fi
-            echo -e "${YELLOW}Fetching from upstream...${NC}"
-            git fetch upstream
-        fi
-
-        # Fetch PR
-        echo -e "${YELLOW}Fetching PR #$pr_number...${NC}"
-        gh pr checkout "$pr_number" -R "$repo" || {
-            echo -e "${RED}Failed to checkout PR${NC}"
-            return 1
-        }
-    fi
-
-    # Determine the correct remote for base branch
-    local base_remote
-    if [ "$is_cross_repo" = "true" ]; then
-        base_remote="upstream"
-    else
-        base_remote="origin"
-    fi
-
-    # Fetch base branch
-    echo -e "${YELLOW}Fetching base branch from $base_remote: $base_branch${NC}"
-    git fetch "$base_remote" "$base_branch" || {
-        echo -e "${RED}Failed to fetch base branch${NC}"
-        return 1
-    }
-
-    # Start rebase
-    echo -e "${YELLOW}Starting rebase onto $base_remote/$base_branch...${NC}"
-    echo ""
-
-    if git rebase "$base_remote/$base_branch"; then
-        echo -e "${GREEN}✓ Rebase completed successfully with no conflicts!${NC}"
-    else
-        echo -e "${YELLOW}Conflicts detected. Starting conflict resolution...${NC}"
-        echo ""
-
-        # Get list of conflicted files
-        conflicted_files=$(git diff --name-only --diff-filter=U)
-
-        if [ -z "$conflicted_files" ]; then
-            echo -e "${RED}Error: Rebase failed but no conflicted files found${NC}"
-            git rebase --abort
-            return 1
-        fi
-
-        echo -e "${BLUE}Conflicted files:${NC}"
-        echo "$conflicted_files" | while read -r file; do
-            echo -e "  ${RED}✗${NC} $file"
-        done
-        echo ""
-
-        # Resolve each conflict
-        while read -r file; do
-            echo -e "${BLUE}Resolving: $file${NC}"
-
-            if ! resolve_with_ediff "$file"; then
-                echo -e "${RED}Failed to resolve conflict in $file${NC}"
-                echo -e "${YELLOW}Options:${NC}"
-                echo -e "  ${YELLOW}1)${NC} Skip this file and continue"
-                echo -e "  ${YELLOW}2)${NC} Abort rebase"
-                echo -e "  ${YELLOW}3)${NC} Open file manually"
-                read -rp "Choice [1-3]: " choice
-
-                case $choice in
-                    1)
-                        echo -e "${YELLOW}Skipping $file${NC}"
-                        continue
-                        ;;
-                    2)
-                        echo -e "${YELLOW}Aborting rebase${NC}"
-                        git rebase --abort
-                        return 1
-                        ;;
-                    3)
-                        ${EDITOR:-vim} "$file"
-                        git add "$file"
-                        ;;
-                    *)
-                        echo -e "${RED}Invalid choice, aborting${NC}"
-                        git rebase --abort
-                        return 1
-                        ;;
-                esac
-            fi
-        done <<< "$conflicted_files"
-
-        # Continue rebase
-        echo -e "${YELLOW}Continuing rebase...${NC}"
-        if git rebase --continue; then
-            echo -e "${GREEN}✓ Rebase completed successfully!${NC}"
-        else
-            echo -e "${RED}Failed to continue rebase${NC}"
-            echo -e "${YELLOW}You may need to resolve remaining conflicts manually${NC}"
-            echo -e "${YELLOW}Working directory: $work_dir${NC}"
-            return 1
-        fi
-    fi
-
-    echo ""
-    echo -e "${GREEN}✓ Conflicts resolved successfully!${NC}"
-    echo ""
-
-    # Push changes
-    if [ "$AUTO_PUSH" = true ]; then
-        echo -e "${YELLOW}Force-pushing changes...${NC}"
-        if git push --force-with-lease; then
-            echo -e "${GREEN}✓ Changes pushed successfully!${NC}"
-        else
-            echo -e "${RED}Failed to push changes${NC}"
-            echo -e "${YELLOW}You may need to push manually from: $work_dir${NC}"
-            return 1
-        fi
-    else
-        echo -e "${YELLOW}Changes not pushed. To push manually:${NC}"
-        echo -e "  cd $work_dir"
-        echo -e "  git push --force-with-lease"
-    fi
-
-    echo ""
-
-    # Cleanup worktree
-    if [ "$USE_WORKTREE" = true ]; then
-        echo -e "${YELLOW}Note: Worktree kept at: $work_dir${NC}"
-        echo -e "${YELLOW}To remove: git worktree remove $work_dir${NC}"
-    fi
-
-    return 0
-}
-
-# Process each selected PR
-while IFS='|' read -r repo pr_number pr_title pr_author pr_branch base_branch pr_url; do
-    pr_number=$(echo "$pr_number" | tr -d '#' | xargs)
-
-    if ! resolve_pr "$repo" "$pr_number" "$pr_title" "$pr_branch" "$base_branch" "$pr_url"; then
-        echo -e "${RED}Failed to resolve PR #$pr_number${NC}"
-        echo ""
-        continue
-    fi
-
-    echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
-    echo ""
-done <<< "$selected_prs"
-
-echo -e "${GREEN}Done!${NC}"
tools/gh-resolve-conflicts/README.md
@@ -1,236 +0,0 @@
-# gh-resolve-conflicts
-
-Interactive tool to scan, identify, and resolve merge conflicts in GitHub pull requests using emacs ediff.
-
-## Features
-
-- **Automatic Scanning**: Searches for all open PRs with merge conflicts across organizations
-- **Interactive Selection**: Uses fzf for multi-select PR picking with preview
-- **Fork-Aware**: Automatically detects and clones your fork, sets up upstream remote
-- **Git Worktrees**: Isolates conflict resolution in separate worktrees (or uses existing repo)
-- **Emacs Ediff Integration**: Launches emacs with ediff-merge for 3-way conflict resolution
-- **Automated Rebase**: Handles git rebase workflow automatically against upstream
-- **Auto-Push by Default**: Automatically force-pushes resolved changes to your fork (disable with `--no-push`)
-- **Uses Your Emacs Config**: Launches ediff with your full emacs configuration
-
-## Installation
-
-### With Nix
-
-Add to your Nix configuration:
-
-```nix
-let
-  gh-resolve-conflicts = pkgs.callPackage ./tools/gh-resolve-conflicts { };
-in {
-  home.packages = [ gh-resolve-conflicts ];
-}
-```
-
-### Manual
-
-```bash
-# Make executable
-chmod +x gh-resolve-conflicts.sh
-
-# Optionally link to PATH
-ln -s "$(pwd)/gh-resolve-conflicts.sh" ~/.local/bin/gh-resolve-conflicts
-```
-
-## Dependencies
-
-- `gh` (GitHub CLI)
-- `fzf` (fuzzy finder, for interactive mode)
-- `jq` (JSON processor)
-- `git`
-- `emacs` (for ediff conflict resolution, falls back to git mergetool if not available)
-
-## Usage
-
-### Interactive Mode
-
-Scan all your PRs in the tektoncd organization:
-
-```bash
-gh-resolve-conflicts -o tektoncd
-```
-
-Scan all your PRs across all organizations:
-
-```bash
-gh-resolve-conflicts
-```
-
-### Direct PR Mode
-
-Resolve a specific PR directly:
-
-```bash
-gh-resolve-conflicts tektoncd/mcp-server#94
-```
-
-### Options
-
-```
--w, --worktree DIR      Create worktrees in DIR (default: /tmp/gh-resolve-conflicts-worktrees)
--n, --no-worktree       Use existing repo instead of creating worktrees
--N, --no-push           Do NOT automatically force-push after resolution (default: auto-push)
--o, --org ORG           Filter PRs by organization
--a, --author AUTHOR     Filter PRs by author (default: @me)
--h, --help              Show help message
-```
-
-### Examples
-
-**Interactive selection (auto-pushes by default):**
-```bash
-gh-resolve-conflicts -o tektoncd
-```
-
-**Interactive selection without auto-push:**
-```bash
-gh-resolve-conflicts -o tektoncd -N
-```
-
-**Use existing repo (no worktree):**
-```bash
-cd ~/src/tektoncd/pipeline
-gh-resolve-conflicts -n tektoncd/pipeline#9197
-```
-
-**Custom worktree directory:**
-```bash
-gh-resolve-conflicts -w ~/tmp/worktrees -o tektoncd
-```
-
-## Workflow
-
-1. **Scan**: Tool searches for open PRs with merge conflicts
-2. **Select**: Interactive fzf interface shows conflicting PRs with preview
-3. **Fork Detection**: Automatically identifies your fork of the upstream repository
-4. **Setup**:
-   - Clones your fork (not upstream)
-   - Adds upstream as a remote
-   - Fetches from both fork and upstream
-5. **Checkout**: Creates worktree and checks out PR branch from your fork
-6. **Rebase**: Attempts rebase against `upstream/base-branch`
-7. **Resolve**: When conflicts occur:
-   - Lists all conflicted files
-   - Launches emacs ediff for each file
-   - Ediff provides 3-way merge interface
-   - Automatically marks files as resolved after ediff
-8. **Complete**: Continues rebase after all conflicts resolved
-9. **Push**: Automatically force-pushes changes to your fork (with `--force-with-lease`, unless `--no-push`)
-
-## Emacs Ediff
-
-When conflicts are detected, the tool launches emacs with ediff-merge using your full emacs configuration:
-
-- **Uses Your Config**: Loads your complete emacs setup (themes, packages, keybindings)
-- **3-way merge interface**: See your changes, their changes, and the common ancestor
-- **Visual conflict resolution**: Navigate conflicts with keyboard shortcuts
-- **Automatic staging**: Resolved files are automatically git-added
-
-### Ediff Controls
-
-- `n` / `p`: Next/previous conflict
-- `a`: Choose variant A (yours)
-- `b`: Choose variant B (theirs)
-- `ab` / `ba`: Combine both variants
-- `q`: Quit ediff (saves and marks resolved)
-
-## Worktree Isolation
-
-By default, the tool creates git worktrees for each PR resolution:
-
-**Benefits:**
-- Isolates work from your main repository
-- Multiple PRs can be resolved in parallel
-- Original repo remains untouched
-- Easy cleanup
-- Automatically handles fork setup (origin = your fork, upstream = original repo)
-
-**Location:**
-```
-/tmp/gh-resolve-conflicts-worktrees/
-├── tektoncd-pipeline/
-│   ├── main/              (bare clone of YOUR fork)
-│   │   ├── origin  -> vdemeester/tektoncd-pipeline (your fork)
-│   │   └── upstream -> tektoncd/pipeline (upstream)
-│   └── pr-9197/           (worktree for PR #9197)
-└── tektoncd-mcp-server/
-    ├── main/              (bare clone of YOUR fork)
-    │   ├── origin  -> vdemeester/tektoncd-mcp-server
-    │   └── upstream -> tektoncd/mcp-server
-    └── pr-94/             (worktree for PR #94)
-```
-
-**Cleanup:**
-```bash
-# Remove specific worktree
-git worktree remove /tmp/gh-resolve-conflicts-worktrees/tektoncd-pipeline/pr-9197
-
-# Or just delete the directory
-rm -rf /tmp/gh-resolve-conflicts-worktrees/
-```
-
-## Use Cases
-
-**Resolve conflicts across multiple repos:**
-```bash
-# Finds all conflicting PRs in tektoncd org and lets you pick which to fix
-gh-resolve-conflicts -o tektoncd
-```
-
-**Quick fix for a single PR:**
-```bash
-# Directly resolve PR #94, auto-pushes when done
-gh-resolve-conflicts tektoncd/mcp-server#94
-```
-
-**Resolve in existing checkout:**
-```bash
-# Use your current repo checkout instead of worktree
-cd ~/src/tektoncd/chains
-gh-resolve-conflicts -n tektoncd/chains#1487
-```
-
-## Troubleshooting
-
-**"emacs not found" warning:**
-- Tool falls back to `git mergetool`
-- Install emacs for better conflict resolution experience
-
-**Ediff doesn't show conflicts:**
-- Some conflicts may need manual resolution
-- Option to open file in $EDITOR is provided
-
-**Rebase fails:**
-- Tool offers options to skip file, abort, or open manually
-- Worktree is preserved for manual intervention
-
-**Can't push after resolution:**
-- Check if you have write access to the repository
-- May need to configure git credentials
-- Use `--force-with-lease` manually if needed
-
-## Related Tools
-
-- `gh-restart-failed`: Restart failed GitHub workflow checks
-- `gh pr checkout`: GitHub CLI built-in PR checkout
-- `gh-pr-worktree`: GitHub CLI extension for PR worktrees
-
-## Contributing
-
-This tool follows the same structure as other tools in `~/src/home/tools/`:
-
-```
-gh-resolve-conflicts/
-├── gh-resolve-conflicts.sh    Main script
-├── default.nix                Nix package definition
-└── README.md                  Documentation
-```
-
-## License
-
-MIT
tools/gh-restart-failed/default.nix
@@ -1,38 +0,0 @@
-{
-  stdenv,
-  lib,
-  makeWrapper,
-  gh,
-  fzf,
-  jq,
-}:
-
-stdenv.mkDerivation {
-  name = "gh-restart-failed";
-  pname = "gh-restart-failed";
-  version = "0.1.0";
-
-  src = ./.;
-
-  nativeBuildInputs = [ makeWrapper ];
-
-  installPhase = ''
-    mkdir -p $out/bin
-    cp gh-restart-failed.sh $out/bin/gh-restart-failed
-    chmod +x $out/bin/gh-restart-failed
-
-    wrapProgram $out/bin/gh-restart-failed \
-      --prefix PATH : ${
-        lib.makeBinPath [
-          gh
-          fzf
-          jq
-        ]
-      }
-  '';
-
-  meta = {
-    description = "List and restart failed GitHub workflow checks on pull requests";
-    platforms = lib.platforms.unix;
-  };
-}
tools/gh-restart-failed/gh-restart-failed.sh
@@ -1,273 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
-
-# Help message
-usage() {
-    cat <<EOF
-Usage: gh-restart-failed [OPTIONS] [REPOSITORY[#PR_NUMBER]]
-
-List pull requests with failed checks and restart selected workflows.
-
-Options:
-    -i, --ignore PATTERN    Ignore workflows matching PATTERN (can be used multiple times)
-    -l, --label LABEL       Filter PRs by label (can be used multiple times)
-    -h, --help             Show this help message
-
-Arguments:
-    REPOSITORY    Optional repository in OWNER/REPO format or path to local repo.
-                  If not provided, uses the current directory's repository.
-                  Can include #PR_NUMBER to directly restart a specific PR (skips interactive selection).
-
-Dependencies:
-    - gh (GitHub CLI)
-    - fzf (fuzzy finder, only needed for interactive mode)
-    - jq (JSON processor)
-
-Note:
-    By default, "Label Checker" workflows are ignored. Use -i to add more patterns.
-
-Examples:
-    gh-restart-failed                                    # Use current repository (interactive)
-    gh-restart-failed owner/repo#123                     # Directly restart PR #123 in owner/repo
-    gh-restart-failed owner/repo                         # Use specific GitHub repository (interactive)
-    gh-restart-failed -i "build" -i "test"              # Ignore build and test workflows
-    gh-restart-failed -l "bug" -l "enhancement"         # Only show PRs with bug OR enhancement labels
-    gh-restart-failed /path/to/repo                     # Use repository at path
-
-EOF
-    exit 0
-}
-
-# Check dependencies
-check_dependencies() {
-    local missing=()
-
-    for cmd in gh jq; do
-        if ! command -v "$cmd" &> /dev/null; then
-            missing+=("$cmd")
-        fi
-    done
-
-    if [ ${#missing[@]} -gt 0 ]; then
-        echo -e "${RED}Error: Missing required dependencies: ${missing[*]}${NC}" >&2
-        echo "Please install them and try again." >&2
-        exit 1
-    fi
-}
-
-# Default ignore patterns
-IGNORE_PATTERNS=("Label Checker")
-LABEL_FILTERS=()
-
-# Parse arguments
-REPO_ARG=""
-PR_NUMBER=""
-while [[ $# -gt 0 ]]; do
-    case $1 in
-        -h|--help)
-            usage
-            ;;
-        -i|--ignore)
-            if [ -n "${2:-}" ]; then
-                IGNORE_PATTERNS+=("$2")
-                shift 2
-            else
-                echo -e "${RED}Error: --ignore requires a pattern argument${NC}" >&2
-                exit 1
-            fi
-            ;;
-        -l|--label)
-            if [ -n "${2:-}" ]; then
-                LABEL_FILTERS+=("$2")
-                shift 2
-            else
-                echo -e "${RED}Error: --label requires a label argument${NC}" >&2
-                exit 1
-            fi
-            ;;
-        -*)
-            echo -e "${RED}Error: Unknown option: $1${NC}" >&2
-            usage
-            ;;
-        *)
-            REPO_ARG="$1"
-            # Check if it contains #PR_NUMBER
-            if [[ "$REPO_ARG" =~ ^(.+)#([0-9]+)$ ]]; then
-                REPO_ARG="${BASH_REMATCH[1]}"
-                PR_NUMBER="${BASH_REMATCH[2]}"
-            fi
-            shift
-            ;;
-    esac
-done
-
-check_dependencies
-
-# Check fzf only if in interactive mode (no PR_NUMBER specified)
-if [ -z "$PR_NUMBER" ] && ! command -v fzf &> /dev/null; then
-    echo -e "${RED}Error: fzf is required for interactive mode${NC}" >&2
-    echo "Please install fzf or specify a PR number directly (e.g., owner/repo#123)" >&2
-    exit 1
-fi
-
-# Determine repository context
-REPO_FLAG=()
-if [ -n "$REPO_ARG" ]; then
-    if [ -d "$REPO_ARG" ]; then
-        # It's a directory path
-        REPO_FLAG=(-R "$(cd "$REPO_ARG" && gh repo view --json nameWithOwner -q .nameWithOwner)")
-    else
-        # Assume it's OWNER/REPO format
-        REPO_FLAG=(-R "$REPO_ARG")
-    fi
-fi
-
-# Show ignored patterns
-if [ ${#IGNORE_PATTERNS[@]} -gt 0 ]; then
-    echo -e "${YELLOW}Ignoring workflows matching: ${IGNORE_PATTERNS[*]}${NC}" >&2
-fi
-
-# Show label filters
-if [ ${#LABEL_FILTERS[@]} -gt 0 ]; then
-    echo -e "${YELLOW}Filtering PRs with labels: ${LABEL_FILTERS[*]}${NC}" >&2
-fi
-
-# If PR_NUMBER is specified, skip interactive selection
-if [ -n "$PR_NUMBER" ]; then
-    echo -e "${BLUE}Fetching PR #$PR_NUMBER...${NC}" >&2
-
-    # Fetch specific PR information
-    pr_info=$(gh pr view "${REPO_FLAG[@]}" "$PR_NUMBER" \
-        --json number,title,headRefName,author \
-        2>/dev/null)
-
-    if [ -z "$pr_info" ]; then
-        echo -e "${RED}Error: PR #$PR_NUMBER not found${NC}" >&2
-        exit 1
-    fi
-
-    pr_title=$(echo "$pr_info" | jq -r '.title')
-    pr_branch=$(echo "$pr_info" | jq -r '.headRefName')
-    pr_author=$(echo "$pr_info" | jq -r '.author.login')
-
-    # Format as if selected from interactive mode
-    selected_prs="#$PR_NUMBER | $pr_title | @$pr_author | $pr_branch | direct"
-else
-    # Interactive mode: Get all open PRs with their check status
-    echo -e "${BLUE}Fetching pull requests...${NC}" >&2
-
-    # Build label filter arguments for gh pr list
-    LABEL_ARGS=()
-    for label in "${LABEL_FILTERS[@]}"; do
-        LABEL_ARGS+=(--label "$label")
-    done
-
-    # Fetch PRs with detailed check information
-    prs_json=$(gh pr list "${REPO_FLAG[@]}" \
-        "${LABEL_ARGS[@]}" \
-        --json number,title,headRefName,author,statusCheckRollup \
-        --limit 100)
-
-    # Filter PRs with failed checks and format for display
-    failed_prs=$(echo "$prs_json" | jq -r '
-        .[] |
-        select(.statusCheckRollup // [] | any(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "STARTUP_FAILURE" or .conclusion == "ACTION_REQUIRED")) |
-        {
-            number: .number,
-            title: .title,
-            branch: .headRefName,
-            author: .author.login,
-            failed_checks: [.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "STARTUP_FAILURE" or .conclusion == "ACTION_REQUIRED")]
-        } |
-        "#\(.number) | \(.title) | @\(.author) | \(.branch) | \(.failed_checks | length) failed"
-    ')
-
-    if [ -z "$failed_prs" ]; then
-        echo -e "${GREEN}No pull requests with failed checks found!${NC}"
-        exit 0
-    fi
-
-    echo -e "${YELLOW}Found pull requests with failed checks:${NC}" >&2
-    echo ""
-
-    # Use fzf to select PRs
-    selected_prs=$(echo "$failed_prs" | fzf \
-        --multi \
-        --ansi \
-        --header="Select pull requests to restart failed workflows (TAB to select multiple, ENTER to confirm)" \
-        --preview="pr_number=\$(echo {} | cut -d'|' -f1 | tr -d '# '); gh pr checks ${REPO_FLAG[*]} \"\$pr_number\" 2>/dev/null | grep -E '(fail|FAILURE|×)' || echo 'Loading...'" \
-        --preview-window=right:60%:wrap \
-        --bind='ctrl-/:toggle-preview' \
-        --height=80%)
-
-    if [ -z "$selected_prs" ]; then
-        echo -e "${YELLOW}No pull requests selected.${NC}"
-        exit 0
-    fi
-fi
-
-echo ""
-echo -e "${BLUE}Processing selected pull requests...${NC}"
-echo ""
-
-# Process each selected PR
-while IFS= read -r pr_line; do
-    pr_number=$(echo "$pr_line" | cut -d'|' -f1 | tr -d '# ' | xargs)
-    pr_title=$(echo "$pr_line" | cut -d'|' -f2 | xargs)
-    pr_branch=$(echo "$pr_line" | cut -d'|' -f4 | xargs)
-
-    echo -e "${BLUE}PR #$pr_number: $pr_title${NC}"
-
-    # Build jq ignore filter
-    ignore_filter=""
-    for pattern in "${IGNORE_PATTERNS[@]}"; do
-        if [ -n "$ignore_filter" ]; then
-            ignore_filter="$ignore_filter and "
-        fi
-        ignore_filter="${ignore_filter}(.name | contains(\"$pattern\") | not)"
-    done
-
-    # Get failed workflow runs for this PR using the branch
-    failed_runs=$(gh run list "${REPO_FLAG[@]}" \
-        --branch "$pr_branch" \
-        --json databaseId,name,conclusion,status,event \
-        --limit 50 \
-        | jq -r "
-        .[] |
-        select(.event == \"pull_request\" and (.conclusion == \"failure\" or .conclusion == \"timed_out\" or .conclusion == \"startup_failure\" or .conclusion == \"action_required\") and ($ignore_filter)) |
-        \"\(.databaseId)|\(.name)|\(.conclusion)\"")
-
-    if [ -z "$failed_runs" ]; then
-        echo -e "${YELLOW}  No failed workflow runs found (may have been restarted already)${NC}"
-        continue
-    fi
-
-    # Restart all failed workflow runs
-    echo -e "${YELLOW}  Restarting failed workflows:${NC}"
-
-    echo "$failed_runs" | while IFS='|' read -r run_id workflow_name status; do
-        echo -e "  ${GREEN}→${NC} Restarting: $workflow_name ($status)"
-
-        rerun_output=$(gh run rerun "${REPO_FLAG[@]}" "$run_id" --failed 2>&1)
-
-        if echo "$rerun_output" | grep -q "created over a month ago"; then
-            echo -e "    ${YELLOW}⚠${NC} Cannot restart: workflow run is too old (>1 month)"
-        elif echo "$rerun_output" | grep -qi "error"; then
-            echo -e "    ${RED}✗${NC} Failed to restart: $rerun_output"
-        else
-            echo -e "    ${GREEN}✓${NC} Restarted successfully"
-        fi
-    done
-
-    echo ""
-done <<< "$selected_prs"
-
-echo -e "${GREEN}Done!${NC}"