Add repo sync

Change-Id: I6b61873c97e9ff46a699151fc52aa937248bcf81
diff --git a/server/app/app.go b/server/app/app.go
index a016c4b..ace8904 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -7,14 +7,17 @@
 	"github.com/iomodo/staff/agent"
 	"github.com/iomodo/staff/config"
 	"github.com/iomodo/staff/git"
+	"github.com/iomodo/staff/tm"
 	"github.com/iomodo/staff/tm/git_tm"
 )
 
 // App type defines application global state
 type App struct {
-	logger  *slog.Logger
-	config  *config.Config
-	manager *agent.Manager
+	logger      *slog.Logger
+	config      *config.Config
+	manager     *agent.Manager
+	git         git.GitInterface
+	taskManager tm.TaskManager
 }
 
 // NewApp creates new App
@@ -29,9 +32,11 @@
 	}
 
 	return &App{
-		logger:  logger,
-		config:  config,
-		manager: manager,
+		logger:      logger,
+		config:      config,
+		manager:     manager,
+		git:         gitInterface,
+		taskManager: taskManager,
 	}, nil
 }
 
diff --git a/server/app/proposal.go b/server/app/proposal.go
index 165f336..8feaded 100644
--- a/server/app/proposal.go
+++ b/server/app/proposal.go
@@ -1,26 +1,105 @@
 package app
 
 import (
+	"context"
 	"fmt"
+	"log/slog"
+	"time"
 
 	"github.com/iomodo/staff/git"
 )
 
 func (a *App) ProposalApproval(body []byte, signature string) error {
-	// Validate the webhook signature
-	if err := git.ValidateSignature(body, signature, a.config.GitHub.WebhookSecret); err != nil {
-		return fmt.Errorf("invalid webhook signature: %w", err)
+	// Set reasonable timeout for webhook processing
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Validate required dependencies
+	if a.git == nil {
+		return fmt.Errorf("git interface not initialized")
+	}
+	if a.taskManager == nil {
+		return fmt.Errorf("task manager not initialized")
 	}
 
-	// Process the webhook payload
+	// Process the webhook payload (includes signature validation)
 	taskID, err := git.ProcessMergeWebhook(body, signature, a.config.GitHub.WebhookSecret)
 	if err != nil {
 		return fmt.Errorf("failed to process webhook: %w", err)
 	}
 
-	// Log the successful approval
 	a.logger.Info("Proposal approved via webhook",
-		"task_id", taskID,
+		slog.String("task_id", taskID),
+		slog.String("repository", fmt.Sprintf("%s/%s", a.config.GitHub.Owner, a.config.GitHub.Repo)),
 	)
+
+	// Synchronize repository to get any new tasks that were added
+	a.syncRepository(ctx)
+	a.logger.Info("Webhook processing completed successfully",
+		slog.String("task_id", taskID),
+	)
+
 	return nil
 }
+
+// syncRepository pulls the remote repository to fetch new tasks
+// This is non-critical for webhook success, so failures are logged but don't fail the webhook
+func (a *App) syncRepository(ctx context.Context) {
+	a.logger.Info("Synchronizing repository to fetch new tasks")
+
+	// Get current branch to determine the upstream branch to pull from
+	currentBranch, err := a.git.GetCurrentBranch(ctx)
+	if err != nil {
+		a.logger.Warn("Failed to get current branch, using default pull",
+			slog.String("error", err.Error()),
+		)
+		// Fallback to simple pull without specifying branch
+		if err := a.git.Pull(ctx, "origin", ""); err != nil {
+			a.logger.Warn("Failed to synchronize repository",
+				slog.String("remote", "origin"),
+				slog.String("error", err.Error()),
+			)
+		} else {
+			a.logger.Info("Successfully synchronized repository",
+				slog.String("remote", "origin"),
+			)
+		}
+		return
+	}
+
+	// Get the upstream branch for the current branch
+	upstreamBranch, err := a.git.GetUpstreamBranch(ctx, currentBranch)
+	if err != nil {
+		a.logger.Warn("Failed to get upstream branch, trying simple pull",
+			slog.String("current_branch", currentBranch),
+			slog.String("error", err.Error()),
+		)
+		// Fallback to simple pull without specifying branch
+		if err := a.git.Pull(ctx, "origin", ""); err != nil {
+			a.logger.Warn("Failed to synchronize repository",
+				slog.String("remote", "origin"),
+				slog.String("error", err.Error()),
+			)
+		} else {
+			a.logger.Info("Successfully synchronized repository",
+				slog.String("remote", "origin"),
+			)
+		}
+		return
+	}
+
+	// Use the upstream branch for pulling
+	if err := a.git.Pull(ctx, "origin", upstreamBranch); err != nil {
+		// Don't fail the entire webhook if git pull fails - agents can still work with existing tasks
+		a.logger.Warn("Failed to synchronize repository",
+			slog.String("remote", "origin"),
+			slog.String("branch", upstreamBranch),
+			slog.String("error", err.Error()),
+		)
+	} else {
+		a.logger.Info("Successfully synchronized repository",
+			slog.String("remote", "origin"),
+			slog.String("branch", upstreamBranch),
+		)
+	}
+}
diff --git a/server/cmd/commands/config_check.go b/server/cmd/commands/config_check.go
index 5256c03..67aac9b 100644
--- a/server/cmd/commands/config_check.go
+++ b/server/cmd/commands/config_check.go
@@ -35,7 +35,7 @@
 
 	// Check Git provider configuration
 	fmt.Printf("\nGit Provider: %s\n", cfg.GetPrimaryGitProvider())
-	
+
 	// Check GitHub configuration
 	if cfg.HasGitHubConfig() {
 		fmt.Printf("✅ GitHub configured (ends with: ...%s)\n", cfg.GitHub.Token[len(cfg.GitHub.Token)-4:])
@@ -43,15 +43,15 @@
 	} else if cfg.GitHub.Token != "" || cfg.GitHub.Owner != "" || cfg.GitHub.Repo != "" {
 		fmt.Println("⚠️  GitHub partially configured (incomplete)")
 	}
-	
-	// Check Gerrit configuration  
+
+	// Check Gerrit configuration
 	if cfg.HasGerritConfig() {
 		fmt.Printf("✅ Gerrit configured (user: %s)\n", cfg.Gerrit.Username)
 		fmt.Printf("   Base URL: %s, Project: %s\n", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
 	} else if cfg.Gerrit.Username != "" || cfg.Gerrit.BaseURL != "" || cfg.Gerrit.Project != "" {
 		fmt.Println("⚠️  Gerrit partially configured (incomplete)")
 	}
-	
+
 	if !cfg.HasGitHubConfig() && !cfg.HasGerritConfig() {
 		fmt.Println("❌ No Git provider configured (need GitHub or Gerrit)")
 	}
diff --git a/server/cmd/commands/webhook.go b/server/cmd/commands/webhook.go
index e62d1ce..da75284 100644
--- a/server/cmd/commands/webhook.go
+++ b/server/cmd/commands/webhook.go
@@ -52,14 +52,14 @@
 	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")
@@ -125,13 +125,13 @@
 		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)
@@ -143,7 +143,7 @@
 		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)
@@ -164,26 +164,26 @@
 	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
 }
 
@@ -192,15 +192,15 @@
 	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
 }
 
@@ -210,7 +210,7 @@
 	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
 }
@@ -221,13 +221,13 @@
 	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 {
@@ -235,17 +235,17 @@
 		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/git/gerrit.go b/server/git/gerrit.go
index d1d029f..d9f1f38 100644
--- a/server/git/gerrit.go
+++ b/server/git/gerrit.go
@@ -152,7 +152,7 @@
 	}
 
 	url := fmt.Sprintf("%s/a/changes/", g.config.BaseURL)
-	
+
 	// Log change creation with structured data
 	g.logger.Info("Creating Gerrit change",
 		slog.String("url", url),
diff --git a/server/git/git.go b/server/git/git.go
index 29cee16..9ee3074 100644
--- a/server/git/git.go
+++ b/server/git/git.go
@@ -32,6 +32,7 @@
 	Checkout(ctx context.Context, ref string) error
 	DeleteBranch(ctx context.Context, name string, force bool) error
 	GetCurrentBranch(ctx context.Context) (string, error)
+	GetUpstreamBranch(ctx context.Context, branch string) (string, error)
 
 	// Commit operations
 	Add(ctx context.Context, paths []string) error
@@ -394,6 +395,22 @@
 	return strings.TrimSpace(output), nil
 }
 
+// GetUpstreamBranch returns the upstream branch for a given branch
+func (g *Git) GetUpstreamBranch(ctx context.Context, branch string) (string, error) {
+	cmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", branch+"@{upstream}")
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "rev-parse")
+	if err != nil {
+		return "", err
+	}
+
+	// The output will be in format "origin/branch-name", we want just "branch-name"
+	upstream := strings.TrimSpace(output)
+	upstream = strings.TrimPrefix(upstream, "origin/")
+
+	return upstream, nil
+}
+
 // Add stages files for commit
 func (g *Git) Add(ctx context.Context, paths []string) error {
 	args := append([]string{"add"}, paths...)
diff --git a/server/git/github.go b/server/git/github.go
index a73c4c6..87ec363 100644
--- a/server/git/github.go
+++ b/server/git/github.go
@@ -114,10 +114,10 @@
 
 // GitHub webhook API types
 type githubWebhookRequest struct {
-	Name   string                `json:"name"`
-	Active bool                  `json:"active"`
-	Events []string              `json:"events"`
-	Config githubWebhookConfig   `json:"config"`
+	Name   string              `json:"name"`
+	Active bool                `json:"active"`
+	Events []string            `json:"events"`
+	Config githubWebhookConfig `json:"config"`
 }
 
 type githubWebhookConfig struct {