task: task-1753636924-a1d4c708 - created

Change-Id: Ic78528c47ae38114b9b7504f1c4a76f95e93eb13
diff --git a/server/agent/simple_manager.go b/server/agent/simple_manager.go
new file mode 100644
index 0000000..27d522d
--- /dev/null
+++ b/server/agent/simple_manager.go
@@ -0,0 +1,565 @@
+package agent
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/iomodo/staff/assignment"
+	"github.com/iomodo/staff/config"
+	"github.com/iomodo/staff/git"
+	"github.com/iomodo/staff/llm"
+	"github.com/iomodo/staff/tm"
+)
+
+// SimpleAgent represents a simplified AI agent for MVP
+type SimpleAgent struct {
+	Name         string
+	Role         string
+	Model        string
+	SystemPrompt string
+	Provider     llm.LLMProvider
+	MaxTokens    *int
+	Temperature  *float64
+}
+
+// SimpleAgentManager manages multiple AI agents with basic Git operations
+type SimpleAgentManager struct {
+	config       *config.Config
+	agents       map[string]*SimpleAgent
+	taskManager  tm.TaskManager
+	autoAssigner *assignment.AutoAssigner
+	prProvider   git.PullRequestProvider
+	cloneManager *git.CloneManager
+	isRunning    map[string]bool
+	stopChannels map[string]chan struct{}
+}
+
+// NewSimpleAgentManager creates a simplified agent manager
+func NewSimpleAgentManager(cfg *config.Config, taskManager tm.TaskManager) (*SimpleAgentManager, error) {
+	// Create auto-assigner
+	autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
+
+	// Create GitHub PR provider
+	githubConfig := git.GitHubConfig{
+		Token: cfg.GitHub.Token,
+	}
+	prProvider := git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
+
+	// Create clone manager for per-agent Git repositories
+	repoURL := fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
+	workspacePath := filepath.Join(".", "workspace")
+	cloneManager := git.NewCloneManager(repoURL, workspacePath)
+
+	manager := &SimpleAgentManager{
+		config:       cfg,
+		agents:       make(map[string]*SimpleAgent),
+		taskManager:  taskManager,
+		autoAssigner: autoAssigner,
+		prProvider:   prProvider,
+		cloneManager: cloneManager,
+		isRunning:    make(map[string]bool),
+		stopChannels: make(map[string]chan struct{}),
+	}
+
+	// Initialize agents
+	if err := manager.initializeAgents(); err != nil {
+		return nil, fmt.Errorf("failed to initialize agents: %w", err)
+	}
+
+	return manager, nil
+}
+
+// initializeAgents creates agent instances from configuration
+func (am *SimpleAgentManager) initializeAgents() error {
+	for _, agentConfig := range am.config.Agents {
+		agent, err := am.createAgent(agentConfig)
+		if err != nil {
+			return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
+		}
+		am.agents[agentConfig.Name] = agent
+	}
+	return nil
+}
+
+// createAgent creates a single agent instance
+func (am *SimpleAgentManager) createAgent(agentConfig config.AgentConfig) (*SimpleAgent, error) {
+	// Load system prompt
+	systemPrompt, err := am.loadSystemPrompt(agentConfig.SystemPromptFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load system prompt: %w", err)
+	}
+
+	// Create LLM provider
+	llmConfig := llm.Config{
+		Provider: llm.ProviderOpenAI,
+		APIKey:   am.config.OpenAI.APIKey,
+		BaseURL:  am.config.OpenAI.BaseURL,
+		Timeout:  am.config.OpenAI.Timeout,
+	}
+	
+	provider, err := llm.CreateProvider(llmConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create LLM provider: %w", err)
+	}
+
+	agent := &SimpleAgent{
+		Name:         agentConfig.Name,
+		Role:         agentConfig.Role,
+		Model:        agentConfig.Model,
+		SystemPrompt: systemPrompt,
+		Provider:     provider,
+		MaxTokens:    agentConfig.MaxTokens,
+		Temperature:  agentConfig.Temperature,
+	}
+
+	return agent, nil
+}
+
+// loadSystemPrompt loads the system prompt from file
+func (am *SimpleAgentManager) loadSystemPrompt(filePath string) (string, error) {
+	content, err := os.ReadFile(filePath)
+	if err != nil {
+		return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
+	}
+	return string(content), nil
+}
+
+// StartAgent starts an agent to process tasks in a loop
+func (am *SimpleAgentManager) StartAgent(agentName string, loopInterval time.Duration) error {
+	agent, exists := am.agents[agentName]
+	if !exists {
+		return fmt.Errorf("agent %s not found", agentName)
+	}
+
+	if am.isRunning[agentName] {
+		return fmt.Errorf("agent %s is already running", agentName)
+	}
+
+	stopChan := make(chan struct{})
+	am.stopChannels[agentName] = stopChan
+	am.isRunning[agentName] = true
+
+	go am.runAgentLoop(agent, loopInterval, stopChan)
+	
+	log.Printf("Started agent %s (%s) with %s model", agentName, agent.Role, agent.Model)
+	return nil
+}
+
+// StopAgent stops a running agent
+func (am *SimpleAgentManager) StopAgent(agentName string) error {
+	if !am.isRunning[agentName] {
+		return fmt.Errorf("agent %s is not running", agentName)
+	}
+
+	close(am.stopChannels[agentName])
+	delete(am.stopChannels, agentName)
+	am.isRunning[agentName] = false
+
+	log.Printf("Stopped agent %s", agentName)
+	return nil
+}
+
+// runAgentLoop runs the main processing loop for an agent
+func (am *SimpleAgentManager) runAgentLoop(agent *SimpleAgent, interval time.Duration, stopChan <-chan struct{}) {
+	ticker := time.NewTicker(interval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-stopChan:
+			log.Printf("Agent %s stopping", agent.Name)
+			return
+		case <-ticker.C:
+			if err := am.processAgentTasks(agent); err != nil {
+				log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
+			}
+		}
+	}
+}
+
+// processAgentTasks processes all assigned tasks for an agent
+func (am *SimpleAgentManager) processAgentTasks(agent *SimpleAgent) error {
+	// Get tasks assigned to this agent
+	tasks, err := am.taskManager.GetTasksByAssignee(agent.Name)
+	if err != nil {
+		return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
+	}
+
+	for _, task := range tasks {
+		if task.Status == tm.StatusPending || task.Status == tm.StatusInProgress {
+			if err := am.processTask(agent, task); err != nil {
+				log.Printf("Error processing task %s: %v", task.ID, err)
+				// Mark task as failed
+				task.Status = tm.StatusFailed
+				if err := am.taskManager.UpdateTask(task); err != nil {
+					log.Printf("Error updating failed task %s: %v", task.ID, err)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// processTask processes a single task with an agent
+func (am *SimpleAgentManager) processTask(agent *SimpleAgent, task *tm.Task) error {
+	ctx := context.Background()
+
+	log.Printf("Agent %s processing task %s: %s", agent.Name, task.ID, task.Title)
+
+	// Mark task as in progress
+	task.Status = tm.StatusInProgress
+	if err := am.taskManager.UpdateTask(task); err != nil {
+		return fmt.Errorf("failed to update task status: %w", err)
+	}
+
+	// Generate solution using LLM
+	solution, err := am.generateSolution(ctx, agent, task)
+	if err != nil {
+		return fmt.Errorf("failed to generate solution: %w", err)
+	}
+
+	// Create Git branch and commit solution
+	branchName := am.generateBranchName(task)
+	if err := am.createAndCommitSolution(branchName, task, solution, agent); err != nil {
+		return fmt.Errorf("failed to commit solution: %w", err)
+	}
+
+	// Create pull request
+	prURL, err := am.createPullRequest(ctx, task, solution, agent, branchName)
+	if err != nil {
+		return fmt.Errorf("failed to create pull request: %w", err)
+	}
+
+	// Update task as completed
+	task.Status = tm.StatusCompleted
+	task.Solution = solution
+	task.PullRequestURL = prURL
+	task.CompletedAt = &time.Time{}
+	*task.CompletedAt = time.Now()
+
+	if err := am.taskManager.UpdateTask(task); err != nil {
+		return fmt.Errorf("failed to update completed task: %w", err)
+	}
+
+	log.Printf("Task %s completed by agent %s. PR: %s", task.ID, agent.Name, prURL)
+	return nil
+}
+
+// generateSolution uses the agent's LLM to generate a solution
+func (am *SimpleAgentManager) generateSolution(ctx context.Context, agent *SimpleAgent, task *tm.Task) (string, error) {
+	prompt := am.buildTaskPrompt(task)
+
+	req := llm.ChatCompletionRequest{
+		Model: agent.Model,
+		Messages: []llm.Message{
+			{
+				Role:    llm.RoleSystem,
+				Content: agent.SystemPrompt,
+			},
+			{
+				Role:    llm.RoleUser,
+				Content: prompt,
+			},
+		},
+		MaxTokens:   agent.MaxTokens,
+		Temperature: agent.Temperature,
+	}
+
+	resp, err := agent.Provider.ChatCompletion(ctx, req)
+	if err != nil {
+		return "", fmt.Errorf("LLM request failed: %w", err)
+	}
+
+	if len(resp.Choices) == 0 {
+		return "", fmt.Errorf("no response from LLM")
+	}
+
+	return resp.Choices[0].Message.Content, nil
+}
+
+// buildTaskPrompt creates a detailed prompt for the LLM
+func (am *SimpleAgentManager) buildTaskPrompt(task *tm.Task) string {
+	return fmt.Sprintf(`Task: %s
+
+Priority: %s
+Description: %s
+
+Please provide a complete solution for this task. Include:
+1. Detailed implementation plan
+2. Code changes needed (if applicable)
+3. Files to be created or modified
+4. Testing considerations
+5. Any dependencies or prerequisites
+
+Your response should be comprehensive and actionable.`,
+		task.Title,
+		task.Priority,
+		task.Description)
+}
+
+// generateBranchName creates a Git branch name for the task
+func (am *SimpleAgentManager) generateBranchName(task *tm.Task) string {
+	// Clean title for use in branch name
+	cleanTitle := strings.ToLower(task.Title)
+	cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
+	cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
+	// Remove special characters
+	var result strings.Builder
+	for _, r := range cleanTitle {
+		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
+			result.WriteRune(r)
+		}
+	}
+	cleanTitle = result.String()
+	
+	// Limit length
+	if len(cleanTitle) > 40 {
+		cleanTitle = cleanTitle[:40]
+	}
+	
+	return fmt.Sprintf("%s%s-%s", am.config.Git.BranchPrefix, task.ID, cleanTitle)
+}
+
+// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
+// Each agent works in its own Git clone, eliminating concurrency issues
+func (am *SimpleAgentManager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *SimpleAgent) error {
+	ctx := context.Background()
+	
+	// Get agent's dedicated Git clone
+	clonePath, err := am.cloneManager.GetAgentClonePath(agent.Name)
+	if err != nil {
+		return fmt.Errorf("failed to get agent clone: %w", err)
+	}
+	
+	log.Printf("Agent %s working in clone: %s", agent.Name, clonePath)
+
+	// Refresh the clone with latest changes
+	if err := am.cloneManager.RefreshAgentClone(agent.Name); err != nil {
+		log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
+	}
+
+	// All Git operations use the agent's 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)
+		}
+	}
+
+	// Create branch
+	cmd = gitCmd("checkout", "-b", branchName)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to create branch: %w", err)
+	}
+
+	// Create solution file in agent's clone
+	solutionDir := filepath.Join(clonePath, "tasks", "solutions")
+	if err := os.MkdirAll(solutionDir, 0755); err != nil {
+		return fmt.Errorf("failed to create solution directory: %w", err)
+	}
+
+	solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
+	solutionContent := fmt.Sprintf(`# Solution for Task: %s
+
+**Agent:** %s (%s)  
+**Model:** %s  
+**Completed:** %s
+
+## Task Description
+%s
+
+## Solution
+%s
+
+---
+*Generated by Staff AI Agent System*
+`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
+
+	if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
+		return fmt.Errorf("failed to write solution file: %w", err)
+	}
+
+	// Stage files
+	relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
+	cmd = gitCmd("add", relativeSolutionFile)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to stage files: %w", err)
+	}
+
+	// Commit changes
+	commitMsg := am.buildCommitMessage(task, agent)
+	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("Agent %s successfully pushed branch %s", agent.Name, branchName)
+	return nil
+}
+
+// buildCommitMessage creates a commit message from template
+func (am *SimpleAgentManager) buildCommitMessage(task *tm.Task, agent *SimpleAgent) string {
+	template := am.config.Git.CommitMessageTemplate
+	
+	replacements := map[string]string{
+		"{task_id}":    task.ID,
+		"{task_title}": task.Title,
+		"{agent_name}": agent.Name,
+		"{solution}":   "See solution file for details",
+	}
+
+	result := template
+	for placeholder, value := range replacements {
+		result = strings.ReplaceAll(result, placeholder, value)
+	}
+
+	return result
+}
+
+// createPullRequest creates a GitHub pull request
+func (am *SimpleAgentManager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *SimpleAgent, branchName string) (string, error) {
+	title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
+	
+	// Build PR description from template
+	description := am.buildPRDescription(task, solution, agent)
+	
+	options := git.PullRequestOptions{
+		Title:       title,
+		Description: description,
+		HeadBranch:  branchName,
+		BaseBranch:  "main",
+		Labels:      []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
+		Draft:       false,
+	}
+
+	pr, err := am.prProvider.CreatePullRequest(ctx, options)
+	if err != nil {
+		return "", fmt.Errorf("failed to create PR: %w", err)
+	}
+
+	return fmt.Sprintf("https://github.com/%s/%s/pull/%d", am.config.GitHub.Owner, am.config.GitHub.Repo, pr.Number), nil
+}
+
+// buildPRDescription creates PR description from template
+func (am *SimpleAgentManager) buildPRDescription(task *tm.Task, solution string, agent *SimpleAgent) string {
+	template := am.config.Git.PRTemplate
+	
+	// Truncate solution for PR if too long
+	truncatedSolution := solution
+	if len(solution) > 1000 {
+		truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
+	}
+	
+	replacements := map[string]string{
+		"{task_id}":          task.ID,
+		"{task_title}":       task.Title,
+		"{task_description}": task.Description,
+		"{agent_name}":       fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
+		"{priority}":         string(task.Priority),
+		"{solution}":         truncatedSolution,
+		"{files_changed}":    fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
+	}
+
+	result := template
+	for placeholder, value := range replacements {
+		result = strings.ReplaceAll(result, placeholder, value)
+	}
+
+	return result
+}
+
+// AutoAssignTask automatically assigns a task to the best matching agent
+func (am *SimpleAgentManager) AutoAssignTask(taskID string) error {
+	task, err := am.taskManager.GetTask(taskID)
+	if err != nil {
+		return fmt.Errorf("failed to get task: %w", err)
+	}
+
+	agentName, err := am.autoAssigner.AssignTask(task)
+	if err != nil {
+		return fmt.Errorf("failed to auto-assign task: %w", err)
+	}
+
+	task.Assignee = agentName
+	if err := am.taskManager.UpdateTask(task); err != nil {
+		return fmt.Errorf("failed to update task assignment: %w", err)
+	}
+
+	explanation := am.autoAssigner.GetRecommendationExplanation(task, agentName)
+	log.Printf("Auto-assigned task %s to %s: %s", taskID, agentName, explanation)
+
+	return nil
+}
+
+// GetAgentStatus returns the status of all agents
+func (am *SimpleAgentManager) GetAgentStatus() map[string]SimpleAgentStatus {
+	status := make(map[string]SimpleAgentStatus)
+	
+	for name, agent := range am.agents {
+		status[name] = SimpleAgentStatus{
+			Name:      agent.Name,
+			Role:      agent.Role,
+			Model:     agent.Model,
+			IsRunning: am.isRunning[name],
+		}
+	}
+	
+	return status
+}
+
+// SimpleAgentStatus represents the status of an agent
+type SimpleAgentStatus struct {
+	Name      string `json:"name"`
+	Role      string `json:"role"`
+	Model     string `json:"model"`
+	IsRunning bool   `json:"is_running"`
+}
+
+// IsAgentRunning checks if an agent is currently running
+func (am *SimpleAgentManager) IsAgentRunning(agentName string) bool {
+	return am.isRunning[agentName]
+}
+
+// Close shuts down the agent manager
+func (am *SimpleAgentManager) Close() error {
+	// Stop all running agents
+	for agentName := range am.isRunning {
+		if am.isRunning[agentName] {
+			am.StopAgent(agentName)
+		}
+	}
+
+	// Close all LLM providers
+	for _, agent := range am.agents {
+		if err := agent.Provider.Close(); err != nil {
+			log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
+		}
+	}
+
+	// Cleanup all agent Git clones
+	if err := am.cloneManager.CleanupAllClones(); err != nil {
+		log.Printf("Error cleaning up agent clones: %v", err)
+	}
+
+	return nil
+}
\ No newline at end of file