Add Github wehbook management

Change-Id: I4b7a23a77838f345d65adf51877fab5978bd6055
diff --git a/server/app/proposal.go b/server/app/proposal.go
index 4b2acc4..165f336 100644
--- a/server/app/proposal.go
+++ b/server/app/proposal.go
@@ -15,7 +15,7 @@
 	// Process the webhook payload
 	taskID, err := git.ProcessMergeWebhook(body, signature, a.config.GitHub.WebhookSecret)
 	if err != nil {
-		return fmt.Errorf("Failed to process webhook: %w", err)
+		return fmt.Errorf("failed to process webhook: %w", err)
 	}
 
 	// Log the successful approval
diff --git a/server/cmd/commands/root.go b/server/cmd/commands/root.go
index e75e134..7a4de91 100644
--- a/server/cmd/commands/root.go
+++ b/server/cmd/commands/root.go
@@ -21,6 +21,7 @@
 	agentManager *agent.Manager
 	taskManager  tm.TaskManager
 	cfg          *config.Config
+	gitInterface git.GitInterface
 )
 
 // Run function starts the application
@@ -75,7 +76,7 @@
 		Level: slog.LevelInfo,
 	}))
 
-	gitInterface := git.New(cfg, logger)
+	gitInterface = git.New(cfg, logger)
 	taskManager = git_tm.NewGitTaskManager(gitInterface, cfg, logger)
 
 	// Initialize agent manager
diff --git a/server/cmd/commands/webhook.go b/server/cmd/commands/webhook.go
new file mode 100644
index 0000000..e62d1ce
--- /dev/null
+++ b/server/cmd/commands/webhook.go
@@ -0,0 +1,251 @@
+package commands
+
+import (
+	"bufio"
+	"context"
+	"crypto/rand"
+	"encoding/hex"
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/iomodo/staff/git"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v3"
+)
+
+var webhookCmd = &cobra.Command{
+	Use:   "webhook",
+	Short: "Webhook management commands",
+	Long: `Manage GitHub webhooks for the Staff AI system.
+
+Webhooks are used to automatically detect when Pull Requests are merged
+and complete corresponding tasks in the Staff system.`,
+}
+
+var webhookConfigureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configure GitHub webhook for PR merge events",
+	Long: `Configure a GitHub webhook to automatically detect when Pull Requests are merged.
+
+This command will:
+1. Generate a webhook secret if not already configured
+2. Register a webhook on GitHub for pull_request events
+3. Update the config.yaml file with the webhook secret
+
+Examples:
+  staff webhook configure                                    # Interactive setup
+  staff webhook configure --webhook-url https://example.com/webhooks/github
+  staff webhook configure --dry-run                         # Show what will be done
+  staff webhook configure --force                           # Force re-registration`,
+	RunE: runWebhookConfigure,
+}
+
+var (
+	webhookURL string
+	dryRun     bool
+	force      bool
+)
+
+func init() {
+	webhookConfigureCmd.Flags().StringVar(&webhookURL, "webhook-url", "", "Webhook URL (if not provided, will prompt)")
+	webhookConfigureCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be done without executing")
+	webhookConfigureCmd.Flags().BoolVar(&force, "force", false, "Force re-registration even if webhook exists")
+	
+	webhookCmd.AddCommand(webhookConfigureCmd)
+	rootCmd.AddCommand(webhookCmd)
+}
+
+func runWebhookConfigure(cmd *cobra.Command, args []string) error {
+	ctx := context.Background()
+	
+	// Validate GitHub configuration
+	if !cfg.HasGitHubConfig() {
+		return fmt.Errorf("GitHub configuration is required. Please configure github.token, github.owner, and github.repo in config.yaml")
+	}
+
+	// Get webhook URL if not provided
+	if webhookURL == "" {
+		var err error
+		webhookURL, err = promptWebhookURL()
+		if err != nil {
+			return fmt.Errorf("failed to get webhook URL: %w", err)
+		}
+	}
+
+	// Validate webhook URL
+	if err := validateWebhookURL(webhookURL); err != nil {
+		return fmt.Errorf("invalid webhook URL: %w", err)
+	}
+
+	// Generate webhook secret if not configured
+	secret := cfg.GitHub.WebhookSecret
+	if secret == "" {
+		var err error
+		secret, err = generateWebhookSecret()
+		if err != nil {
+			return fmt.Errorf("failed to generate webhook secret: %w", err)
+		}
+		fmt.Printf("Generated new webhook secret: %s\n", secret)
+	} else {
+		fmt.Printf("Using existing webhook secret: %s\n", secret)
+	}
+
+	if dryRun {
+		fmt.Printf("\nDry run mode - showing what would be done:\n")
+		fmt.Printf("- Webhook URL: %s\n", webhookURL)
+		fmt.Printf("- Events: [pull_request]\n")
+		fmt.Printf("- Secret: %s\n", secret)
+		fmt.Printf("- Repository: %s/%s\n", cfg.GitHub.Owner, cfg.GitHub.Repo)
+		if cfg.GitHub.WebhookSecret == "" {
+			fmt.Printf("- Would update config.yaml with webhook secret\n")
+		}
+		fmt.Printf("- Would register webhook on GitHub\n")
+		return nil
+	}
+
+	// Check existing webhooks
+	fmt.Printf("Checking existing webhooks...\n")
+	webhooks, err := gitInterface.ListWebhooks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to list existing webhooks: %w", err)
+	}
+
+	// Look for existing webhook with our URL
+	var existingWebhook *git.GitHubWebhookResponse
+	for i, webhook := range webhooks {
+		if webhook.Config.URL == webhookURL && webhook.Name == "web" {
+			existingWebhook = &webhooks[i]
+			break
+		}
+	}
+
+	if existingWebhook != nil {
+		if !force {
+			return fmt.Errorf("webhook already exists with URL %s (ID: %d). Use --force to update it", webhookURL, existingWebhook.ID)
+		}
+		
+		fmt.Printf("Updating existing webhook (ID: %d)...\n", existingWebhook.ID)
+		updatedWebhook, err := gitInterface.UpdateWebhook(ctx, existingWebhook.ID, webhookURL, secret)
+		if err != nil {
+			return fmt.Errorf("failed to update webhook: %w", err)
+		}
+		
+		fmt.Printf("Successfully updated webhook!\n")
+		fmt.Printf("- Webhook ID: %d\n", updatedWebhook.ID)
+		fmt.Printf("- URL: %s\n", updatedWebhook.Config.URL)
+		fmt.Printf("- Events: %v\n", updatedWebhook.Events)
+		fmt.Printf("- Secret: %s\n", secret)
+	} else {
+		fmt.Printf("Creating new webhook...\n")
+		newWebhook, err := gitInterface.CreateWebhook(ctx, webhookURL, secret)
+		if err != nil {
+			return fmt.Errorf("failed to create webhook: %w", err)
+		}
+		
+		fmt.Printf("Successfully created webhook!\n")
+		fmt.Printf("- Webhook ID: %d\n", newWebhook.ID)
+		fmt.Printf("- URL: %s\n", newWebhook.Config.URL)
+		fmt.Printf("- Events: %v\n", newWebhook.Events)
+		fmt.Printf("- Secret: %s\n", secret)
+	}
+
+	// Update config file if webhook secret changed
+	if cfg.GitHub.WebhookSecret != secret {
+		fmt.Printf("Updating config.yaml with webhook secret...\n")
+		if err := updateConfigWebhookSecret(secret); err != nil {
+			return fmt.Errorf("failed to update config file: %w", err)
+		}
+		fmt.Printf("Config file updated successfully!\n")
+	}
+
+	fmt.Printf("\nWebhook configuration complete!\n")
+	fmt.Printf("\nWebhook Secret: %s\n", secret)
+	fmt.Printf("Please save this secret for your webhook endpoint configuration.\n")
+	fmt.Printf("\nYour Staff system will now automatically detect merged PRs and complete tasks.\n")
+	
+	return nil
+}
+
+func promptWebhookURL() (string, error) {
+	reader := bufio.NewReader(os.Stdin)
+	
+	// Show server listen address if configured
+	if cfg.Server.ListenAddress != "" {
+		fmt.Printf("Detected server listen address: %s\n", cfg.Server.ListenAddress)
+		fmt.Printf("Suggested webhook URL: http://%s/webhooks/github\n", cfg.Server.ListenAddress)
+		fmt.Printf("(Note: Use https:// and a public domain for production)\n")
+	}
+	
+	fmt.Printf("Enter webhook URL (e.g., https://your-domain.com/webhooks/github): ")
+	url, err := reader.ReadString('\n')
+	if err != nil {
+		return "", err
+	}
+	
+	return strings.TrimSpace(url), nil
+}
+
+func validateWebhookURL(webhookURL string) error {
+	parsedURL, err := url.Parse(webhookURL)
+	if err != nil {
+		return fmt.Errorf("invalid URL format: %w", err)
+	}
+	
+	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
+		return fmt.Errorf("URL must use http or https scheme")
+	}
+	
+	if parsedURL.Host == "" {
+		return fmt.Errorf("URL must include a host")
+	}
+	
+	return nil
+}
+
+func generateWebhookSecret() (string, error) {
+	// Generate 32 bytes of random data
+	bytes := make([]byte, 32)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", fmt.Errorf("failed to generate random bytes: %w", err)
+	}
+	
+	// Encode as hex string
+	return hex.EncodeToString(bytes), nil
+}
+
+func updateConfigWebhookSecret(secret string) error {
+	// Read current config file
+	configData, err := os.ReadFile("config.yaml")
+	if err != nil {
+		return fmt.Errorf("failed to read config file: %w", err)
+	}
+	
+	// Parse YAML
+	var configMap map[string]interface{}
+	if err := yaml.Unmarshal(configData, &configMap); err != nil {
+		return fmt.Errorf("failed to parse config YAML: %w", err)
+	}
+	
+	// Update webhook secret
+	github, ok := configMap["github"].(map[string]interface{})
+	if !ok {
+		github = make(map[string]interface{})
+		configMap["github"] = github
+	}
+	github["webhook_secret"] = secret
+	
+	// Marshal back to YAML
+	updatedData, err := yaml.Marshal(configMap)
+	if err != nil {
+		return fmt.Errorf("failed to marshal config YAML: %w", err)
+	}
+	
+	// Write back to file
+	if err := os.WriteFile("config.yaml", updatedData, 0644); err != nil {
+		return fmt.Errorf("failed to write config file: %w", err)
+	}
+	
+	return nil
+}
\ No newline at end of file
diff --git a/server/config.yaml b/server/config.yaml
index 5ce3fec..dabe621 100644
--- a/server/config.yaml
+++ b/server/config.yaml
@@ -2,7 +2,7 @@
 # This is a minimal config for testing the MVP
 
 server:
-  listen_address: ":9325"
+  listen_address: "9325"
 
 openai:
   api_key: "${OPENAI_API_KEY}"
diff --git a/server/git/git.go b/server/git/git.go
index 4cd7508..29cee16 100644
--- a/server/git/git.go
+++ b/server/git/git.go
@@ -69,6 +69,11 @@
 	RefreshAgentClone(agentName string) error
 	CleanupAgentClone(agentName string) error
 	CleanupAllClones() error
+
+	// Webhook operations (GitHub only)
+	ListWebhooks(ctx context.Context) ([]GitHubWebhookResponse, error)
+	CreateWebhook(ctx context.Context, webhookURL, secret string) (*GitHubWebhookResponse, error)
+	UpdateWebhook(ctx context.Context, webhookID int, webhookURL, secret string) (*GitHubWebhookResponse, error)
 }
 
 // Status represents the current state of the repository
@@ -679,6 +684,35 @@
 	return g.cloneManager.CleanupAllClones()
 }
 
+// Webhook operations (GitHub only)
+
+// ListWebhooks lists existing webhooks for the repository
+func (g *Git) ListWebhooks(ctx context.Context) ([]GitHubWebhookResponse, error) {
+	webhookProvider, ok := g.prProvider.(WebhookProvider)
+	if !ok {
+		return nil, &GitError{Command: "ListWebhooks", Output: "webhook operations not supported by current provider"}
+	}
+	return webhookProvider.ListWebhooks(ctx)
+}
+
+// CreateWebhook creates a new webhook for the repository
+func (g *Git) CreateWebhook(ctx context.Context, webhookURL, secret string) (*GitHubWebhookResponse, error) {
+	webhookProvider, ok := g.prProvider.(WebhookProvider)
+	if !ok {
+		return nil, &GitError{Command: "CreateWebhook", Output: "webhook operations not supported by current provider"}
+	}
+	return webhookProvider.CreateWebhook(ctx, webhookURL, secret)
+}
+
+// UpdateWebhook updates an existing webhook
+func (g *Git) UpdateWebhook(ctx context.Context, webhookID int, webhookURL, secret string) (*GitHubWebhookResponse, error) {
+	webhookProvider, ok := g.prProvider.(WebhookProvider)
+	if !ok {
+		return nil, &GitError{Command: "UpdateWebhook", Output: "webhook operations not supported by current provider"}
+	}
+	return webhookProvider.UpdateWebhook(ctx, webhookID, webhookURL, secret)
+}
+
 // Helper methods
 
 func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
@@ -982,3 +1016,10 @@
 	ClosePullRequest(ctx context.Context, id string) error
 	MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
 }
+
+// WebhookProvider defines the interface for webhook operations
+type WebhookProvider interface {
+	ListWebhooks(ctx context.Context) ([]GitHubWebhookResponse, error)
+	CreateWebhook(ctx context.Context, webhookURL, secret string) (*GitHubWebhookResponse, error)
+	UpdateWebhook(ctx context.Context, webhookID int, webhookURL, secret string) (*GitHubWebhookResponse, error)
+}
diff --git a/server/git/github.go b/server/git/github.go
index 1d37b1d..a73c4c6 100644
--- a/server/git/github.go
+++ b/server/git/github.go
@@ -112,6 +112,29 @@
 	MergeMethod string `json:"merge_method,omitempty"`
 }
 
+// GitHub webhook API types
+type githubWebhookRequest struct {
+	Name   string                `json:"name"`
+	Active bool                  `json:"active"`
+	Events []string              `json:"events"`
+	Config githubWebhookConfig   `json:"config"`
+}
+
+type githubWebhookConfig struct {
+	URL         string `json:"url"`
+	ContentType string `json:"content_type"`
+	Secret      string `json:"secret"`
+}
+
+type GitHubWebhookResponse struct {
+	ID     int                 `json:"id"`
+	Name   string              `json:"name"`
+	Active bool                `json:"active"`
+	Events []string            `json:"events"`
+	Config githubWebhookConfig `json:"config"`
+	URL    string              `json:"url"`
+}
+
 // CreatePullRequest creates a new pull request on GitHub
 func (g *GitHubPullRequestProvider) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
 	reqBody := githubCreatePRRequest{
@@ -415,3 +438,140 @@
 		URL:        fmt.Sprintf("https://github.com/%s/%s/pull/%d", g.owner, g.repo, githubPR.Number),
 	}
 }
+
+// ListWebhooks lists existing webhooks for the repository
+func (g *GitHubPullRequestProvider) ListWebhooks(ctx context.Context) ([]GitHubWebhookResponse, error) {
+	url := fmt.Sprintf("%s/repos/%s/%s/hooks", g.config.BaseURL, g.owner, g.repo)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Authorization", "token "+g.config.Token)
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+	resp, err := g.config.HTTPClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to make request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		var errorBody bytes.Buffer
+		_, _ = errorBody.ReadFrom(resp.Body)
+		return nil, fmt.Errorf("GitHub API error: %d - %s", resp.StatusCode, errorBody.String())
+	}
+
+	var webhooks []GitHubWebhookResponse
+	if err := json.NewDecoder(resp.Body).Decode(&webhooks); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return webhooks, nil
+}
+
+// CreateWebhook creates a new webhook for the repository
+func (g *GitHubPullRequestProvider) CreateWebhook(ctx context.Context, webhookURL, secret string) (*GitHubWebhookResponse, error) {
+	reqBody := githubWebhookRequest{
+		Name:   "web",
+		Active: true,
+		Events: []string{"pull_request"},
+		Config: githubWebhookConfig{
+			URL:         webhookURL,
+			ContentType: "json",
+			Secret:      secret,
+		},
+	}
+
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	g.logger.Info("Creating GitHub webhook",
+		slog.String("url", fmt.Sprintf("%s/repos/%s/%s/hooks", g.config.BaseURL, g.owner, g.repo)),
+		slog.String("webhook_url", webhookURL),
+		slog.Any("events", reqBody.Events))
+
+	url := fmt.Sprintf("%s/repos/%s/%s/hooks", g.config.BaseURL, g.owner, g.repo)
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Authorization", "token "+g.config.Token)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+	resp, err := g.config.HTTPClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to make request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusCreated {
+		var errorBody bytes.Buffer
+		_, _ = errorBody.ReadFrom(resp.Body)
+		return nil, fmt.Errorf("GitHub API error: %d - %s", resp.StatusCode, errorBody.String())
+	}
+
+	var webhook GitHubWebhookResponse
+	if err := json.NewDecoder(resp.Body).Decode(&webhook); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return &webhook, nil
+}
+
+// UpdateWebhook updates an existing webhook
+func (g *GitHubPullRequestProvider) UpdateWebhook(ctx context.Context, webhookID int, webhookURL, secret string) (*GitHubWebhookResponse, error) {
+	reqBody := githubWebhookRequest{
+		Name:   "web",
+		Active: true,
+		Events: []string{"pull_request"},
+		Config: githubWebhookConfig{
+			URL:         webhookURL,
+			ContentType: "json",
+			Secret:      secret,
+		},
+	}
+
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	g.logger.Info("Updating GitHub webhook",
+		slog.Int("webhook_id", webhookID),
+		slog.String("webhook_url", webhookURL),
+		slog.Any("events", reqBody.Events))
+
+	url := fmt.Sprintf("%s/repos/%s/%s/hooks/%d", g.config.BaseURL, g.owner, g.repo, webhookID)
+	req, err := http.NewRequestWithContext(ctx, "PATCH", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Authorization", "token "+g.config.Token)
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+	resp, err := g.config.HTTPClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to make request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		var errorBody bytes.Buffer
+		_, _ = errorBody.ReadFrom(resp.Body)
+		return nil, fmt.Errorf("GitHub API error: %d - %s", resp.StatusCode, errorBody.String())
+	}
+
+	var webhook GitHubWebhookResponse
+	if err := json.NewDecoder(resp.Body).Decode(&webhook); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return &webhook, nil
+}
diff --git a/server/staff b/server/staff
index cfd99cf..22c9770 100755
--- a/server/staff
+++ b/server/staff
Binary files differ
diff --git a/server/tm/git_tm/git_task_manager.go b/server/tm/git_tm/git_task_manager.go
index 2e9fce3..a49a866 100644
--- a/server/tm/git_tm/git_task_manager.go
+++ b/server/tm/git_tm/git_task_manager.go
@@ -1103,7 +1103,7 @@
 		cleanTitle = cleanTitle[:40]
 	}
 
-	return fmt.Sprintf("%s%s-%s", prefix, task.ID, cleanTitle)
+	return fmt.Sprintf("%s/%s-%s", prefix, task.ID, cleanTitle)
 }
 
 // buildSolutionPRDescription creates PR description from template