Add Github integration to subtasks

Change-Id: If382f2f5238e9d2323d6c2761d3d70a626ff9065
diff --git a/server/agent/manager.go b/server/agent/manager.go
index ab625bf..45b0965 100644
--- a/server/agent/manager.go
+++ b/server/agent/manager.go
@@ -107,6 +107,10 @@
 		firstAgent.Provider,
 		m.taskManager,
 		agentRoles,
+		m.prProvider,
+		m.config.GitHub.Owner,
+		m.config.GitHub.Repo,
+		m.cloneManager,
 	)
 
 	return nil
diff --git a/server/subtasks/service.go b/server/subtasks/service.go
index 710dfaf..5f99f74 100644
--- a/server/subtasks/service.go
+++ b/server/subtasks/service.go
@@ -5,8 +5,13 @@
 	"encoding/json"
 	"fmt"
 	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
 	"strings"
+	"time"
 
+	"github.com/iomodo/staff/git"
 	"github.com/iomodo/staff/llm"
 	"github.com/iomodo/staff/tm"
 )
@@ -16,14 +21,22 @@
 	llmProvider    llm.LLMProvider
 	taskManager    tm.TaskManager
 	agentRoles     []string // Available agent roles for assignment
+	prProvider     git.PullRequestProvider // GitHub PR provider
+	githubOwner    string
+	githubRepo     string
+	cloneManager   *git.CloneManager
 }
 
 // NewSubtaskService creates a new subtask service
-func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string) *SubtaskService {
+func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string, prProvider git.PullRequestProvider, githubOwner, githubRepo string, cloneManager *git.CloneManager) *SubtaskService {
 	return &SubtaskService{
-		llmProvider: provider,
-		taskManager: taskManager,
-		agentRoles:  agentRoles,
+		llmProvider:  provider,
+		taskManager:  taskManager,
+		agentRoles:   agentRoles,
+		prProvider:   prProvider,
+		githubOwner:  githubOwner,
+		githubRepo:   githubRepo,
+		cloneManager: cloneManager,
 	}
 }
 
@@ -402,16 +415,40 @@
 
 // GenerateSubtaskPR creates a PR with the proposed subtasks
 func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
-	// Generate markdown content for the PR
+	if s.prProvider == nil {
+		return "", fmt.Errorf("PR provider not configured")
+	}
+
+	// Generate branch name for subtask proposal
+	branchName := fmt.Sprintf("subtasks/%s-proposal", analysis.ParentTaskID)
+
+	// Create Git branch and commit subtask proposal
+	if err := s.createSubtaskBranch(ctx, analysis, branchName); err != nil {
+		return "", fmt.Errorf("failed to create subtask branch: %w", err)
+	}
+
+	// Generate PR content
 	prContent := s.generateSubtaskPRContent(analysis)
-	
-	// This would typically create a Git branch and PR
-	// For now, we'll return a mock PR URL
-	prURL := fmt.Sprintf("https://github.com/example/repo/pull/subtasks-%s", analysis.ParentTaskID)
-	
+	title := fmt.Sprintf("Subtask Proposal: %s", analysis.ParentTaskID)
+
+	// Create the pull request
+	options := git.PullRequestOptions{
+		Title:       title,
+		Description: prContent,
+		HeadBranch:  branchName,
+		BaseBranch:  "main",
+		Labels:      []string{"subtasks", "proposal", "ai-generated"},
+		Draft:       false,
+	}
+
+	pr, err := s.prProvider.CreatePullRequest(ctx, options)
+	if err != nil {
+		return "", fmt.Errorf("failed to create PR: %w", err)
+	}
+
+	prURL := fmt.Sprintf("https://github.com/%s/%s/pull/%d", s.githubOwner, s.githubRepo, pr.Number)
 	log.Printf("Generated subtask proposal PR: %s", prURL)
-	log.Printf("PR Content:\n%s", prContent)
-	
+
 	return prURL, nil
 }
 
@@ -454,6 +491,129 @@
 	return content.String()
 }
 
+// createSubtaskBranch creates a Git branch with subtask proposal content
+func (s *SubtaskService) createSubtaskBranch(ctx context.Context, analysis *tm.SubtaskAnalysis, branchName string) error {
+	if s.cloneManager == nil {
+		return fmt.Errorf("clone manager not configured")
+	}
+
+	// Get a temporary clone for creating the subtask branch
+	clonePath, err := s.cloneManager.GetAgentClonePath("subtask-service")
+	if err != nil {
+		return fmt.Errorf("failed to get clone path: %w", err)
+	}
+
+	// All Git operations use the clone directory
+	gitCmd := func(args ...string) *exec.Cmd {
+		return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
+	}
+
+	// Ensure we're on main branch before creating new branch
+	cmd := gitCmd("checkout", "main")
+	if err := cmd.Run(); err != nil {
+		// Try master branch if main doesn't exist
+		cmd = gitCmd("checkout", "master")
+		if err := cmd.Run(); err != nil {
+			return fmt.Errorf("failed to checkout main/master branch: %w", err)
+		}
+	}
+
+	// Pull latest changes
+	cmd = gitCmd("pull", "origin")
+	if err := cmd.Run(); err != nil {
+		log.Printf("Warning: failed to pull latest changes: %v", err)
+	}
+
+	// Create and checkout new branch
+	cmd = gitCmd("checkout", "-b", branchName)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to create branch: %w", err)
+	}
+
+	// Create subtask proposal file
+	proposalDir := filepath.Join(clonePath, "tasks", "subtasks")
+	if err := os.MkdirAll(proposalDir, 0755); err != nil {
+		return fmt.Errorf("failed to create proposal directory: %w", err)
+	}
+
+	proposalFile := filepath.Join(proposalDir, fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
+	proposalContent := s.generateSubtaskProposalFile(analysis)
+
+	if err := os.WriteFile(proposalFile, []byte(proposalContent), 0644); err != nil {
+		return fmt.Errorf("failed to write proposal file: %w", err)
+	}
+
+	// Stage the file
+	relativeFile := filepath.Join("tasks", "subtasks", fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
+	cmd = gitCmd("add", relativeFile)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to stage files: %w", err)
+	}
+
+	// Commit changes
+	commitMsg := fmt.Sprintf("Subtask proposal for task %s\n\nGenerated by Staff AI Agent System\nProposed %d subtasks with %d new agents", 
+		analysis.ParentTaskID, len(analysis.Subtasks), len(analysis.AgentCreations))
+	cmd = gitCmd("commit", "-m", commitMsg)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to commit: %w", err)
+	}
+
+	// Push branch
+	cmd = gitCmd("push", "-u", "origin", branchName)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to push branch: %w", err)
+	}
+
+	log.Printf("Created subtask proposal branch: %s", branchName)
+	return nil
+}
+
+// generateSubtaskProposalFile creates the content for the subtask proposal file
+func (s *SubtaskService) generateSubtaskProposalFile(analysis *tm.SubtaskAnalysis) string {
+	var content strings.Builder
+	
+	content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
+	content.WriteString(fmt.Sprintf("**Generated:** %s\n\n", time.Now().Format(time.RFC3339)))
+	content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
+
+	if len(analysis.AgentCreations) > 0 {
+		content.WriteString("## Proposed New Agents\n\n")
+		for i, agent := range analysis.AgentCreations {
+			content.WriteString(fmt.Sprintf("### %d. %s Agent\n", i+1, strings.Title(agent.Role)))
+			content.WriteString(fmt.Sprintf("- **Skills:** %s\n", strings.Join(agent.Skills, ", ")))
+			content.WriteString(fmt.Sprintf("- **Description:** %s\n", agent.Description))
+			content.WriteString(fmt.Sprintf("- **Justification:** %s\n\n", agent.Justification))
+		}
+	}
+
+	content.WriteString("## Proposed Subtasks\n\n")
+	for i, subtask := range analysis.Subtasks {
+		content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
+		content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+		content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+		content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+		if len(subtask.RequiredSkills) > 0 {
+			content.WriteString(fmt.Sprintf("- **Required Skills:** %s\n", strings.Join(subtask.RequiredSkills, ", ")))
+		}
+		if len(subtask.Dependencies) > 0 {
+			content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", strings.Join(subtask.Dependencies, ", ")))
+		}
+		content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+	}
+
+	content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
+	content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
+
+	if analysis.RiskAssessment != "" {
+		content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
+	}
+
+	content.WriteString("---\n\n")
+	content.WriteString("*This proposal was generated by the Staff AI Agent System. Review and approve to proceed with subtask creation.*\n")
+	
+	return content.String()
+}
+
 // Close cleans up the service
 func (s *SubtaskService) Close() error {
 	if s.llmProvider != nil {
diff --git a/server/subtasks/service_test.go b/server/subtasks/service_test.go
index ade62fc..158bcdc 100644
--- a/server/subtasks/service_test.go
+++ b/server/subtasks/service_test.go
@@ -80,7 +80,7 @@
 	mockProvider := NewMockLLMProvider([]string{})
 	agentRoles := []string{"backend", "frontend", "qa"}
 	
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	if service == nil {
 		t.Fatal("NewSubtaskService returned nil")
@@ -106,7 +106,7 @@
 
 	mockProvider := NewMockLLMProvider([]string{decisionResponse})
 	agentRoles := []string{"backend", "frontend", "qa"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	// Test the parseSubtaskDecision method directly since ShouldGenerateSubtasks is used by manager
 	decision, err := service.parseSubtaskDecision(decisionResponse)
@@ -165,7 +165,7 @@
 
 	mockProvider := NewMockLLMProvider([]string{jsonResponse})
 	agentRoles := []string{"backend", "frontend", "qa", "ceo"} // Include CEO for agent creation
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	task := &tm.Task{
 		ID:          "test-task-123",
@@ -266,7 +266,7 @@
 	
 	mockProvider := NewMockLLMProvider([]string{invalidResponse})
 	agentRoles := []string{"backend", "frontend"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	task := &tm.Task{
 		ID:    "test-task-123",
@@ -302,7 +302,7 @@
 
 	mockProvider := NewMockLLMProvider([]string{jsonResponse})
 	agentRoles := []string{"backend", "frontend"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	task := &tm.Task{
 		ID:    "test-task-123",
@@ -322,7 +322,7 @@
 
 func TestGenerateSubtaskPR(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
-	service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+	service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)
 	
 	analysis := &tm.SubtaskAnalysis{
 		ParentTaskID:        "task-123",
@@ -342,21 +342,21 @@
 		},
 	}
 	
-	prURL, err := service.GenerateSubtaskPR(context.Background(), analysis)
-	if err != nil {
-		t.Fatalf("GenerateSubtaskPR failed: %v", err)
+	// Test that PR generation fails when no PR provider is configured
+	_, err := service.GenerateSubtaskPR(context.Background(), analysis)
+	if err == nil {
+		t.Error("Expected error when PR provider not configured, got nil")
 	}
 	
-	expectedURL := "https://github.com/example/repo/pull/subtasks-task-123"
-	if prURL != expectedURL {
-		t.Errorf("Expected PR URL %s, got %s", expectedURL, prURL)
+	if !strings.Contains(err.Error(), "PR provider not configured") {
+		t.Errorf("Expected 'PR provider not configured' error, got: %v", err)
 	}
 }
 
 func TestBuildSubtaskAnalysisPrompt(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
 	agentRoles := []string{"backend", "frontend", "qa"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	task := &tm.Task{
 		Title:       "Build authentication system",
@@ -387,7 +387,7 @@
 func TestGetSubtaskAnalysisSystemPrompt(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
 	agentRoles := []string{"backend", "frontend", "qa", "devops"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	systemPrompt := service.getSubtaskAnalysisSystemPrompt()
 	
@@ -411,7 +411,7 @@
 func TestIsValidAgentRole(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
 	agentRoles := []string{"backend", "frontend", "qa"}
-	service := NewSubtaskService(mockProvider, nil, agentRoles)
+	service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil)
 	
 	if !service.isValidAgentRole("backend") {
 		t.Error("'backend' should be a valid agent role")
@@ -432,7 +432,7 @@
 
 func TestParseSubtaskAnalysis_Priority(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
-	service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+	service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)
 	
 	tests := []struct {
 		input    string
@@ -484,7 +484,7 @@
 
 func TestClose(t *testing.T) {
 	mockProvider := NewMockLLMProvider([]string{})
-	service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+	service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)
 	
 	err := service.Close()
 	if err != nil {
@@ -511,7 +511,7 @@
 }`
 
 	mockProvider := NewMockLLMProvider([]string{jsonResponse})
-	service := NewSubtaskService(mockProvider, nil, []string{"backend", "frontend"})
+	service := NewSubtaskService(mockProvider, nil, []string{"backend", "frontend"}, nil, "example", "repo", nil)
 	
 	task := &tm.Task{
 		ID:          "benchmark-task",