Add Agent manager
Change-Id: Iaa68e9228165bd274f9c5be9d4320ef49a009ca8
diff --git a/server/agent/agent.go b/server/agent/agent.go
deleted file mode 100644
index 1b27831..0000000
--- a/server/agent/agent.go
+++ /dev/null
@@ -1,580 +0,0 @@
-package agent
-
-import (
- "context"
- "fmt"
- "log/slog"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- _ "github.com/iomodo/staff/llm/openai" // Import for side effects (registers provider)
- "github.com/iomodo/staff/tm"
-)
-
-// AgentConfig contains configuration for the agent
-type AgentConfig struct {
- Name string
- Role string
- GitUsername string
- GitEmail string
- WorkingDir string
-
- // LLM Configuration
- LLMProvider llm.Provider
- LLMModel string
- LLMConfig llm.Config
-
- // System prompt for the agent
- SystemPrompt string
-
- // Task Manager Configuration
- TaskManager tm.TaskManager
-
- // Git Configuration
- GitRepoPath string
- GitRemote string
- GitBranch string
-
- // Gerrit Configuration
- GerritEnabled bool
- GerritConfig GerritConfig
-}
-
-// GerritConfig holds configuration for Gerrit operations
-type GerritConfig struct {
- Username string
- Password string // Can be HTTP password or API token
- BaseURL string
- Project string
-}
-
-// Agent represents an AI agent that can process tasks
-type Agent struct {
- Config AgentConfig
- llmProvider llm.LLMProvider
- gitInterface git.GitInterface
- ctx context.Context
- cancel context.CancelFunc
- logger *slog.Logger
-}
-
-// NewAgent creates a new agent instance
-func NewAgent(config AgentConfig, logger *slog.Logger) (*Agent, error) {
- // Validate configuration
- if err := validateConfig(config); err != nil {
- return nil, fmt.Errorf("invalid config: %w", err)
- }
-
- // Create LLM provider
- llmProvider, err := llm.CreateProvider(config.LLMConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to create LLM provider: %w", err)
- }
-
- // Create git interface
- var gitInterface git.GitInterface
- if config.GerritEnabled {
- // Create Gerrit pull request provider
- gerritPRProvider := git.NewGerritPullRequestProvider(config.GerritConfig.Project, git.GerritConfig{
- Username: config.GerritConfig.Username,
- Password: config.GerritConfig.Password,
- BaseURL: config.GerritConfig.BaseURL,
- HTTPClient: nil, // Will use default client
- })
-
- // Create git interface with Gerrit pull request provider
- gitConfig := git.GitConfig{
- Timeout: 30 * time.Second,
- PullRequestProvider: gerritPRProvider,
- }
- gitInterface = git.NewGitWithPullRequests(config.GitRepoPath, gitConfig, gerritPRProvider, logger)
- } else {
- // Use default git interface (GitHub)
- gitInterface = git.DefaultGit(config.GitRepoPath)
- }
-
- // Create context with cancellation
- ctx, cancel := context.WithCancel(context.Background())
-
- agent := &Agent{
- Config: config,
- llmProvider: llmProvider,
- gitInterface: gitInterface,
- ctx: ctx,
- cancel: cancel,
- logger: logger,
- }
-
- return agent, nil
-}
-
-// validateConfig validates the agent configuration
-func validateConfig(config AgentConfig) error {
- if config.Name == "" {
- return fmt.Errorf("agent name is required")
- }
- if config.Role == "" {
- return fmt.Errorf("agent role is required")
- }
- if config.WorkingDir == "" {
- return fmt.Errorf("working directory is required")
- }
- if config.SystemPrompt == "" {
- return fmt.Errorf("system prompt is required")
- }
- if config.TaskManager == nil {
- return fmt.Errorf("task manager is required")
- }
- if config.GitRepoPath == "" {
- return fmt.Errorf("git repository path is required")
- }
- return nil
-}
-
-const (
- // Agent polling intervals
- TaskPollingInterval = 60 * time.Second
- ErrorRetryInterval = 30 * time.Second
- DefaultGitTimeout = 30 * time.Second
- DefaultContextTimeout = 5 * time.Minute
- DefaultMaxTaskRetries = 3
- DefaultLLMMaxTokens = 4000
- DefaultLLMTemperature = 0.7
-)
-
-// Run starts the agent's main loop
-func (a *Agent) Run() error {
- a.logger.Info("Starting agent", slog.String("name", a.Config.Name), slog.String("role", a.Config.Role))
- defer a.logger.Info("Agent stopped", slog.String("name", a.Config.Name))
-
- // Initialize git repository if needed
- if err := a.initializeGit(); err != nil {
- return fmt.Errorf("failed to initialize git: %w", err)
- }
-
- // Main agent loop
- for {
- select {
- case <-a.ctx.Done():
- return a.ctx.Err()
- default:
- if err := a.processNextTask(); err != nil {
- a.logger.Error("Error processing task", slog.String("error", err.Error()))
- time.Sleep(ErrorRetryInterval)
- }
- }
- }
-}
-
-// Stop stops the agent
-func (a *Agent) Stop() {
- a.logger.Info("Stopping agent", slog.String("name", a.Config.Name))
- a.cancel()
- if a.llmProvider != nil {
- a.llmProvider.Close()
- }
-}
-
-// initializeGit initializes the git repository
-func (a *Agent) initializeGit() error {
- ctx := context.Background()
-
- if err := a.ensureRepository(ctx); err != nil {
- return err
- }
-
- if err := a.ensureRemoteOrigin(ctx); err != nil {
- return err
- }
-
- if err := a.ensureTargetBranch(ctx); err != nil {
- return err
- }
-
- return nil
-}
-
-// ensureRepository ensures the git repository is initialized
-func (a *Agent) ensureRepository(ctx context.Context) error {
- isRepo, err := a.gitInterface.IsRepository(ctx, a.Config.GitRepoPath)
- if err != nil {
- return fmt.Errorf("failed to check repository: %w", err)
- }
-
- if !isRepo {
- if err := a.gitInterface.Init(ctx, a.Config.GitRepoPath); err != nil {
- return fmt.Errorf("failed to initialize repository: %w", err)
- }
- }
-
- return nil
-}
-
-// ensureRemoteOrigin ensures the remote origin is configured
-func (a *Agent) ensureRemoteOrigin(ctx context.Context) error {
- remotes, err := a.gitInterface.ListRemotes(ctx)
- if err != nil {
- return fmt.Errorf("failed to list remotes: %w", err)
- }
-
- // Check if origin already exists
- for _, remote := range remotes {
- if remote.Name == "origin" {
- return nil
- }
- }
-
- // Add remote origin
- remoteURL := a.buildRemoteURL()
- if err := a.gitInterface.AddRemote(ctx, "origin", remoteURL); err != nil {
- return fmt.Errorf("failed to add remote origin: %w", err)
- }
-
- return nil
-}
-
-// buildRemoteURL builds the appropriate remote URL based on configuration
-func (a *Agent) buildRemoteURL() string {
- if !a.Config.GerritEnabled {
- return a.Config.GitRemote
- }
-
- // Build Gerrit URL
- if strings.HasPrefix(a.Config.GerritConfig.BaseURL, "https://") {
- return fmt.Sprintf("%s/%s.git", a.Config.GerritConfig.BaseURL, a.Config.GerritConfig.Project)
- }
-
- // SSH format
- return fmt.Sprintf("ssh://%s@%s:29418/%s.git",
- a.Config.GerritConfig.Username,
- strings.TrimPrefix(a.Config.GerritConfig.BaseURL, "https://"),
- a.Config.GerritConfig.Project)
-}
-
-// ensureTargetBranch ensures the agent is on the target branch
-func (a *Agent) ensureTargetBranch(ctx context.Context) error {
- if a.Config.GitBranch == "" {
- return nil
- }
-
- currentBranch, err := a.gitInterface.GetCurrentBranch(ctx)
- if err != nil {
- return fmt.Errorf("failed to get current branch: %w", err)
- }
-
- if currentBranch == a.Config.GitBranch {
- a.logger.Info("Already on target branch", slog.String("branch", a.Config.GitBranch))
- return nil
- }
-
- return a.checkoutOrCreateBranch(ctx, a.Config.GitBranch)
-}
-
-// checkoutOrCreateBranch attempts to checkout a branch, creating it if it doesn't exist
-func (a *Agent) checkoutOrCreateBranch(ctx context.Context, branchName string) error {
- if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
- if a.isBranchNotFoundError(err) {
- if createErr := a.gitInterface.CreateBranch(ctx, branchName, ""); createErr != nil {
- return fmt.Errorf("failed to create branch %s: %w", branchName, createErr)
- }
- return nil
- }
- return fmt.Errorf("failed to checkout branch %s: %w", branchName, err)
- }
- return nil
-}
-
-// isBranchNotFoundError checks if the error indicates a branch doesn't exist
-func (a *Agent) isBranchNotFoundError(err error) bool {
- errMsg := err.Error()
- return strings.Contains(errMsg, "did not match any file(s) known to git") ||
- strings.Contains(errMsg, "not found") ||
- strings.Contains(errMsg, "unknown revision") ||
- strings.Contains(errMsg, "reference is not a tree") ||
- strings.Contains(errMsg, "pathspec") ||
- strings.Contains(errMsg, "fatal: invalid reference")
-}
-
-// processNextTask processes the next available task
-func (a *Agent) processNextTask() error {
- ctx := context.Background()
-
- // Get tasks assigned to this agent
- taskList, err := a.Config.TaskManager.GetTasksByOwner(ctx, a.Config.Name, 0, 10)
- a.logger.Info("Total number of Tasks", slog.String("agent", a.Config.Name), slog.Any("tasks", taskList.TotalCount))
- if err != nil {
- return fmt.Errorf("failed to get tasks: %w", err)
- }
-
- // Find a task that's ready to be worked on
- var taskToProcess *tm.Task
- for _, task := range taskList.Tasks {
- if task.Status == tm.StatusToDo {
- taskToProcess = task
- break
- }
- }
-
- a.logger.Info("Task to process", slog.Any("task", taskToProcess))
-
- if taskToProcess == nil {
- // No tasks to process, wait a bit
- time.Sleep(TaskPollingInterval)
- return nil
- }
-
- a.logger.Info("Processing task", slog.String("id", taskToProcess.ID), slog.String("title", taskToProcess.Title))
-
- // Start the task
- startedTask, err := a.Config.TaskManager.StartTask(ctx, taskToProcess.ID)
- if err != nil {
- return fmt.Errorf("failed to start task: %w", err)
- }
-
- // Process the task with LLM
- solution, err := a.processTaskWithLLM(startedTask)
- if err != nil {
- // Mark task as failed or retry
- a.logger.Error("Failed to process task with LLM", slog.String("error", err.Error()))
- return err
- }
-
- // Create PR with the solution
- if err := a.createPullRequest(startedTask, solution); err != nil {
- return fmt.Errorf("failed to create pull request: %w", err)
- }
-
- // Complete the task
- if _, err := a.Config.TaskManager.CompleteTask(ctx, startedTask.ID); err != nil {
- return fmt.Errorf("failed to complete task: %w", err)
- }
-
- a.logger.Info("Successfully completed task", slog.String("id", startedTask.ID))
- return nil
-}
-
-// processTaskWithLLM sends the task to the LLM and gets a solution
-func (a *Agent) processTaskWithLLM(task *tm.Task) (string, error) {
- ctx := context.Background()
-
- // Prepare the prompt
- prompt := a.buildTaskPrompt(task)
-
- // Create chat completion request
- req := llm.ChatCompletionRequest{
- Model: a.Config.LLMModel,
- Messages: []llm.Message{
- {
- Role: llm.RoleSystem,
- Content: a.Config.SystemPrompt,
- },
- {
- Role: llm.RoleUser,
- Content: prompt,
- },
- },
- MaxTokens: intPtr(DefaultLLMMaxTokens),
- Temperature: float64Ptr(DefaultLLMTemperature),
- }
-
- // Get response from LLM
- resp, err := a.llmProvider.ChatCompletion(ctx, req)
- if err != nil {
- return "", fmt.Errorf("LLM chat completion failed: %w", err)
- }
-
- if len(resp.Choices) == 0 {
- return "", fmt.Errorf("no response from LLM")
- }
-
- return resp.Choices[0].Message.Content, nil
-}
-
-// buildTaskPrompt builds the prompt for the LLM based on the task
-func (a *Agent) buildTaskPrompt(task *tm.Task) string {
- var prompt strings.Builder
-
- prompt.WriteString(fmt.Sprintf("Task ID: %s\n", task.ID))
- prompt.WriteString(fmt.Sprintf("Title: %s\n", task.Title))
- prompt.WriteString(fmt.Sprintf("Priority: %s\n", task.Priority))
-
- if task.Description != "" {
- prompt.WriteString(fmt.Sprintf("Description: %s\n", task.Description))
- }
-
- if task.DueDate != nil {
- prompt.WriteString(fmt.Sprintf("Due Date: %s\n", task.DueDate.Format("2006-01-02")))
- }
-
- prompt.WriteString("\nPlease provide a detailed solution for this task. ")
- prompt.WriteString("Include any code, documentation, or other deliverables as needed. ")
- prompt.WriteString("Format your response appropriately for the type of task.")
-
- return prompt.String()
-}
-
-// createPullRequest creates a pull request with the solution
-func (a *Agent) createPullRequest(task *tm.Task, solution string) error {
- ctx := context.Background()
-
- // Generate branch name
- branchName := a.generateBranchName(task)
-
- // Create and checkout to new branch
- if err := a.gitInterface.CreateBranch(ctx, branchName, ""); err != nil {
- return fmt.Errorf("failed to create branch: %w", err)
- }
-
- if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
- return fmt.Errorf("failed to checkout branch: %w", err)
- }
-
- // Create solution file
- solutionPath := filepath.Join(a.Config.WorkingDir, fmt.Sprintf("task-%s-solution.md", task.ID))
- solutionContent := a.formatSolution(task, solution)
-
- if err := os.WriteFile(solutionPath, []byte(solutionContent), 0644); err != nil {
- return fmt.Errorf("failed to write solution file: %w", err)
- }
-
- // Add and commit the solution
- if err := a.gitInterface.Add(ctx, []string{solutionPath}); err != nil {
- return fmt.Errorf("failed to add solution file: %w", err)
- }
-
- commitMessage := fmt.Sprintf("feat: Complete task %s - %s\n\n%s", task.ID, task.Title, a.formatPullRequestDescription(task, solution))
- if err := a.gitInterface.Commit(ctx, commitMessage, git.CommitOptions{
- Author: &git.Author{
- Name: a.Config.GitUsername,
- Email: a.Config.GitEmail,
- Time: time.Now(),
- },
- }); err != nil {
- return fmt.Errorf("failed to commit solution: %w", err)
- }
-
- if a.Config.GerritEnabled {
- // For Gerrit: Push to refs/for/BRANCH to create a change
- gerritRef := fmt.Sprintf("refs/for/%s", a.Config.GitBranch)
- if err := a.gitInterface.Push(ctx, "origin", gerritRef, git.PushOptions{}); err != nil {
- return fmt.Errorf("failed to push to Gerrit: %w", err)
- }
- a.logger.Info("Created Gerrit change for task", slog.String("id", task.ID), slog.String("ref", gerritRef))
- } else {
- // For GitHub: Push branch and create PR
- if err := a.gitInterface.Push(ctx, "origin", branchName, git.PushOptions{SetUpstream: true}); err != nil {
- return fmt.Errorf("failed to push branch: %w", err)
- }
-
- // Create pull request using the git interface
- prOptions := git.PullRequestOptions{
- Title: fmt.Sprintf("Complete task %s: %s", task.ID, task.Title),
- Description: a.formatPullRequestDescription(task, solution),
- BaseBranch: a.Config.GitBranch,
- HeadBranch: branchName,
- BaseRepo: a.Config.GerritConfig.Project,
- HeadRepo: a.Config.GerritConfig.Project,
- }
-
- pr, err := a.gitInterface.CreatePullRequest(ctx, prOptions)
- if err != nil {
- return fmt.Errorf("failed to create pull request: %w", err)
- }
-
- a.logger.Info("Created pull request for task", slog.String("id", task.ID), slog.String("title", pr.Title), slog.String("pr_id", pr.ID))
- }
-
- return nil
-}
-
-// generateBranchName generates a branch name for the task
-func (a *Agent) generateBranchName(task *tm.Task) string {
- // Clean the task title for branch name
- cleanTitle := strings.ReplaceAll(task.Title, " ", "-")
- cleanTitle = strings.ToLower(cleanTitle)
-
- // Remove special characters that are not allowed in git branch names
- // Keep only alphanumeric characters and hyphens
- var result strings.Builder
- for _, char := range cleanTitle {
- if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' {
- result.WriteRune(char)
- }
- }
- cleanTitle = result.String()
-
- // Remove consecutive hyphens
- for strings.Contains(cleanTitle, "--") {
- cleanTitle = strings.ReplaceAll(cleanTitle, "--", "-")
- }
-
- // Remove leading and trailing hyphens
- cleanTitle = strings.Trim(cleanTitle, "-")
-
- // Limit length
- if len(cleanTitle) > 50 {
- cleanTitle = cleanTitle[:50]
- // Ensure we don't end with a hyphen after truncation
- cleanTitle = strings.TrimSuffix(cleanTitle, "-")
- }
-
- return fmt.Sprintf("task/%s-%s", task.ID, cleanTitle)
-}
-
-// formatSolution formats the solution for the pull request
-func (a *Agent) formatSolution(task *tm.Task, solution string) string {
- var content strings.Builder
-
- content.WriteString(fmt.Sprintf("# Task Solution: %s\n\n", task.Title))
- content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
- content.WriteString(fmt.Sprintf("**Agent:** %s (%s)\n", a.Config.Name, a.Config.Role))
- content.WriteString(fmt.Sprintf("**Completed:** %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
-
- content.WriteString("## Task Description\n\n")
- content.WriteString(task.Description)
- content.WriteString("\n\n")
-
- content.WriteString("## Solution\n\n")
- content.WriteString(solution)
- content.WriteString("\n\n")
-
- content.WriteString("---\n")
- content.WriteString("*This solution was generated by AI Agent*\n")
-
- return content.String()
-}
-
-// formatPullRequestDescription formats the description for the pull request
-func (a *Agent) formatPullRequestDescription(task *tm.Task, solution string) string {
- var content strings.Builder
-
- content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
- content.WriteString(fmt.Sprintf("**Title:** %s\n", task.Title))
- content.WriteString(fmt.Sprintf("**Priority:** %s\n", task.Priority))
-
- if task.Description != "" {
- content.WriteString(fmt.Sprintf("**Description:** %s\n", task.Description))
- }
-
- if task.DueDate != nil {
- content.WriteString(fmt.Sprintf("**Due Date:** %s\n", task.DueDate.Format("2006-01-02")))
- }
-
- content.WriteString("\n**Solution:**\n\n")
- content.WriteString(solution)
-
- return content.String()
-}
-
-// ptr helpers for cleaner code
-func intPtr(i int) *int {
- return &i
-}
-
-func float64Ptr(f float64) *float64 {
- return &f
-}
diff --git a/server/agent/agent_test.go b/server/agent/agent_test.go
deleted file mode 100644
index 6ec2adc..0000000
--- a/server/agent/agent_test.go
+++ /dev/null
@@ -1,421 +0,0 @@
-package agent
-
-import (
- "context"
- "log/slog"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- "github.com/iomodo/staff/tm"
- "github.com/iomodo/staff/tm/git_tm"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// MockLLMProvider implements LLMProvider for testing
-type MockLLMProvider struct{}
-
-func (m *MockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
- return &llm.ChatCompletionResponse{
- ID: "mock-response-id",
- Model: req.Model,
- Choices: []llm.ChatCompletionChoice{
- {
- Index: 0,
- Message: llm.Message{
- Role: llm.RoleAssistant,
- Content: "This is a mock response for testing purposes.",
- },
- FinishReason: "stop",
- },
- },
- Usage: llm.Usage{
- PromptTokens: 10,
- CompletionTokens: 20,
- TotalTokens: 30,
- },
- Provider: llm.ProviderOpenAI,
- }, nil
-}
-
-func (m *MockLLMProvider) CreateEmbeddings(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
- return &llm.EmbeddingResponse{
- Object: "list",
- Data: []llm.Embedding{
- {
- Object: "embedding",
- Embedding: []float64{0.1, 0.2, 0.3},
- Index: 0,
- },
- },
- Usage: llm.Usage{
- PromptTokens: 5,
- TotalTokens: 5,
- },
- Model: req.Model,
- Provider: llm.ProviderOpenAI,
- }, nil
-}
-
-func (m *MockLLMProvider) Close() error {
- return nil
-}
-
-// MockLLMFactory implements ProviderFactory for testing
-type MockLLMFactory struct{}
-
-func (f *MockLLMFactory) CreateProvider(config llm.Config) (llm.LLMProvider, error) {
- return &MockLLMProvider{}, nil
-}
-
-func (f *MockLLMFactory) SupportsProvider(provider llm.Provider) bool {
- return provider == llm.ProviderOpenAI
-}
-
-func setupTestAgent(t *testing.T) (*Agent, func()) {
- // Create temporary directories
- tempDir, err := os.MkdirTemp("", "agent-test")
- require.NoError(t, err)
-
- tasksDir := filepath.Join(tempDir, "tasks")
- workspaceDir := filepath.Join(tempDir, "workspace")
- codeRepoDir := filepath.Join(tempDir, "code-repo")
-
- // Create directories
- require.NoError(t, os.MkdirAll(tasksDir, 0755))
- require.NoError(t, os.MkdirAll(workspaceDir, 0755))
- require.NoError(t, os.MkdirAll(codeRepoDir, 0755))
-
- // Initialize git repositories
- gitInterface := git.DefaultGit(tasksDir)
- ctx := context.Background()
-
- err = gitInterface.Init(ctx, tasksDir)
- require.NoError(t, err)
-
- // Set git user config
- userConfig := git.UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = gitInterface.SetUserConfig(ctx, userConfig)
- require.NoError(t, err)
-
- // Create logger for testing
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, tasksDir, logger)
-
- // Create LLM config (using a mock configuration)
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "test-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent config
- config := AgentConfig{
- Name: "test-agent",
- Role: "Test Engineer",
- GitUsername: "test-agent",
- GitEmail: "test-agent@test.com",
- WorkingDir: workspaceDir,
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-3.5-turbo",
- LLMConfig: llmConfig,
- SystemPrompt: "You are a test agent. Provide simple, clear solutions.",
- TaskManager: taskManager,
- GitRepoPath: codeRepoDir,
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create logger for testing
- logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create agent with mock LLM provider
- agent := &Agent{
- Config: config,
- llmProvider: &MockLLMProvider{},
- gitInterface: git.DefaultGit(codeRepoDir),
- ctx: context.Background(),
- cancel: func() {},
- logger: logger,
- }
-
- cleanup := func() {
- agent.Stop()
- os.RemoveAll(tempDir)
- }
-
- return agent, cleanup
-}
-
-func TestNewAgent(t *testing.T) {
- // Create temporary directories
- tempDir, err := os.MkdirTemp("", "agent-test")
- require.NoError(t, err)
- defer os.RemoveAll(tempDir)
-
- tasksDir := filepath.Join(tempDir, "tasks")
- workspaceDir := filepath.Join(tempDir, "workspace")
- codeRepoDir := filepath.Join(tempDir, "code-repo")
-
- // Create directories
- require.NoError(t, os.MkdirAll(tasksDir, 0755))
- require.NoError(t, os.MkdirAll(workspaceDir, 0755))
- require.NoError(t, os.MkdirAll(codeRepoDir, 0755))
-
- // Initialize git repositories
- gitInterface := git.DefaultGit(tasksDir)
- ctx := context.Background()
-
- err = gitInterface.Init(ctx, tasksDir)
- require.NoError(t, err)
-
- // Set git user config
- userConfig := git.UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = gitInterface.SetUserConfig(ctx, userConfig)
- require.NoError(t, err)
-
- // Create logger for testing
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, tasksDir, logger)
-
- // Create LLM config (using a mock configuration)
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "test-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent config
- config := AgentConfig{
- Name: "test-agent",
- Role: "Test Engineer",
- GitUsername: "test-agent",
- GitEmail: "test-agent@test.com",
- WorkingDir: workspaceDir,
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-3.5-turbo",
- LLMConfig: llmConfig,
- SystemPrompt: "You are a test agent. Provide simple, clear solutions.",
- TaskManager: taskManager,
- GitRepoPath: codeRepoDir,
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create logger for testing
- logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create agent using NewAgent function
- agent, err := NewAgent(config, logger)
- require.NoError(t, err)
- defer agent.Stop()
-
- assert.NotNil(t, agent)
- assert.Equal(t, "test-agent", agent.Config.Name)
- assert.Equal(t, "Test Engineer", agent.Config.Role)
-}
-
-func TestValidateConfig(t *testing.T) {
- // Test valid config
- validConfig := AgentConfig{
- Name: "test",
- Role: "test",
- WorkingDir: "/tmp",
- SystemPrompt: "test",
- TaskManager: &git_tm.GitTaskManager{},
- GitRepoPath: "/tmp",
- }
-
- err := validateConfig(validConfig)
- assert.NoError(t, err)
-
- // Test invalid configs
- testCases := []struct {
- name string
- config AgentConfig
- }{
- {"empty name", AgentConfig{Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty role", AgentConfig{Name: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty working dir", AgentConfig{Name: "test", Role: "test", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty system prompt", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
- {"nil task manager", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
- {"empty git repo path", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}}},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- err := validateConfig(tc.config)
- assert.Error(t, err)
- })
- }
-}
-
-func TestGenerateBranchName(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Implement User Authentication",
- }
-
- branchName := agent.generateBranchName(task)
- assert.Contains(t, branchName, "task-123")
- assert.Contains(t, branchName, "implement-user-authentication")
-}
-
-func TestBuildTaskPrompt(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- dueDate := time.Now().AddDate(0, 0, 7)
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task",
- Priority: tm.PriorityHigh,
- DueDate: &dueDate,
- }
-
- prompt := agent.buildTaskPrompt(task)
- assert.Contains(t, prompt, "task-123")
- assert.Contains(t, prompt, "Test Task")
- assert.Contains(t, prompt, "This is a test task")
- assert.Contains(t, prompt, "high")
-}
-
-func TestFormatSolution(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task description",
- Priority: tm.PriorityMedium,
- }
-
- solution := "This is the solution to the task."
- formatted := agent.formatSolution(task, solution)
-
- assert.Contains(t, formatted, "# Task Solution: Test Task")
- assert.Contains(t, formatted, "**Task ID:** task-123")
- assert.Contains(t, formatted, "**Agent:** test-agent (Test Engineer)")
- assert.Contains(t, formatted, "## Task Description")
- assert.Contains(t, formatted, "This is a test task description")
- assert.Contains(t, formatted, "## Solution")
- assert.Contains(t, formatted, "This is the solution to the task.")
- assert.Contains(t, formatted, "*This solution was generated by AI Agent*")
-}
-
-func TestAgentStop(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- // Test that Stop doesn't panic
- assert.NotPanics(t, func() {
- agent.Stop()
- })
-}
-
-func TestGenerateBranchNameWithSpecialCharacters(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- testCases := []struct {
- title string
- expected string
- }{
- {
- title: "Simple Task",
- expected: "task/task-123-simple-task",
- },
- {
- title: "Task with (parentheses) and [brackets]",
- expected: "task/task-123-task-with-parentheses-and-brackets",
- },
- {
- title: "Very Long Task Title That Should Be Truncated Because It Exceeds The Maximum Length Allowed For Branch Names",
- expected: "task/task-123-very-long-task-title-that-should-be-truncated-beca",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.title, func(t *testing.T) {
- task := &tm.Task{
- ID: "task-123",
- Title: tc.title,
- }
-
- branchName := agent.generateBranchName(task)
- assert.Equal(t, tc.expected, branchName)
- })
- }
-}
-
-func TestProcessTaskWithLLM(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task",
- Priority: tm.PriorityHigh,
- }
-
- solution, err := agent.processTaskWithLLM(task)
- assert.NoError(t, err)
- assert.Contains(t, solution, "mock response")
-}
-
-func TestMockLLMProvider(t *testing.T) {
- mockProvider := &MockLLMProvider{}
-
- // Test ChatCompletion
- req := llm.ChatCompletionRequest{
- Model: "gpt-3.5-turbo",
- Messages: []llm.Message{
- {Role: llm.RoleUser, Content: "Hello"},
- },
- }
-
- resp, err := mockProvider.ChatCompletion(context.Background(), req)
- assert.NoError(t, err)
- assert.NotNil(t, resp)
- assert.Equal(t, "gpt-3.5-turbo", resp.Model)
- assert.Len(t, resp.Choices, 1)
- assert.Contains(t, resp.Choices[0].Message.Content, "mock response")
-
- // Test CreateEmbeddings
- embedReq := llm.EmbeddingRequest{
- Input: "test",
- Model: "text-embedding-ada-002",
- }
-
- embedResp, err := mockProvider.CreateEmbeddings(context.Background(), embedReq)
- assert.NoError(t, err)
- assert.NotNil(t, embedResp)
- assert.Len(t, embedResp.Data, 1)
- assert.Len(t, embedResp.Data[0].Embedding, 3)
-
- // Test Close
- err = mockProvider.Close()
- assert.NoError(t, err)
-}
diff --git a/server/agent/example.go b/server/agent/example.go
deleted file mode 100644
index a7faea9..0000000
--- a/server/agent/example.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package agent
-
-import (
- "context"
- "log/slog"
- "os"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- "github.com/iomodo/staff/tm"
- "github.com/iomodo/staff/tm/git_tm"
-)
-
-// ExampleAgent demonstrates how to create and run an agent
-func ExampleAgent() {
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create git interface for task management
- gitInterface := git.DefaultGit("./tasks-repo")
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, "./tasks-repo", logger)
-
- // Create LLM configuration
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key-here", // Replace with actual API key
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent configuration
- config := AgentConfig{
- Name: "backend-engineer-1",
- Role: "Backend Engineer",
- GitUsername: "backend-agent",
- GitEmail: "backend-agent@company.com",
- WorkingDir: "./workspace",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llmConfig,
- SystemPrompt: `You are a skilled backend engineer. Your role is to:
-1. Analyze tasks and provide technical solutions
-2. Write clean, maintainable code
-3. Consider performance, security, and scalability
-4. Provide clear documentation for your solutions
-5. Follow best practices and coding standards
-
-When responding to tasks, provide:
-- Detailed technical analysis
-- Code examples where appropriate
-- Implementation considerations
-- Testing recommendations
-- Documentation suggestions`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create agent
- agent, err := NewAgent(config, logger)
- if err != nil {
- logger.Error("Failed to create agent", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Create a sample task
- ctx := context.Background()
- task, err := taskManager.CreateTask(ctx, &tm.TaskCreateRequest{
- Title: "Implement user authentication API",
- Description: "Create a REST API endpoint for user authentication with JWT tokens. Include login, logout, and token refresh functionality.",
- OwnerID: config.Name,
- Priority: tm.PriorityHigh,
- })
- if err != nil {
- logger.Error("Failed to create task", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Created task", slog.String("id", task.ID))
-
- // Run the agent (this will process tasks in an infinite loop)
- go func() {
- if err := agent.Run(); err != nil {
- logger.Error("Agent stopped with error", slog.String("error", err.Error()))
- }
- }()
-
- // Let the agent run for a while
- time.Sleep(5 * time.Minute)
-
- // Stop the agent
- agent.Stop()
-}
-
-// ExampleMultipleAgents demonstrates how to create multiple agents with different roles
-func ExampleMultipleAgents() {
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create shared git interface for task management
- gitInterface := git.DefaultGit("./tasks-repo")
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, "./tasks-repo", logger)
-
- // Create agents with different roles
- agents := []AgentConfig{
- {
- Name: "backend-engineer-1",
- Role: "Backend Engineer",
- GitUsername: "backend-agent",
- GitEmail: "backend-agent@company.com",
- WorkingDir: "./workspace/backend",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `You are a backend engineer. Focus on:
-- API design and implementation
-- Database design and optimization
-- Security best practices
-- Performance optimization
-- Code quality and testing`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- {
- Name: "frontend-engineer-1",
- Role: "Frontend Engineer",
- GitUsername: "frontend-agent",
- GitEmail: "frontend-agent@company.com",
- WorkingDir: "./workspace/frontend",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `You are a frontend engineer. Focus on:
-- User interface design and implementation
-- React/Vue/Angular development
-- Responsive design and accessibility
-- Performance optimization
-- User experience best practices`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- {
- Name: "product-manager-1",
- Role: "Product Manager",
- GitUsername: "pm-agent",
- GitEmail: "pm-agent@company.com",
- WorkingDir: "./workspace/product",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `You are a product manager. Focus on:
-- Product strategy and roadmap
-- User research and requirements gathering
-- Feature prioritization and planning
-- Stakeholder communication
-- Product documentation and specifications`,
- TaskManager: taskManager,
- GitRepoPath: "./docs-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- }
-
- // Create and start all agents
- for _, config := range agents {
- agent, err := NewAgent(config, logger)
- if err != nil {
- logger.Error("Failed to create agent", slog.String("name", config.Name), slog.String("error", err.Error()))
- continue
- }
-
- go func(agent *Agent, name string) {
- logger.Info("Starting agent", slog.String("name", name))
- if err := agent.Run(); err != nil {
- logger.Error("Agent stopped with error", slog.String("name", name), slog.String("error", err.Error()))
- }
- }(agent, config.Name)
- }
-
- // Let agents run for a while
- time.Sleep(10 * time.Minute)
-}
diff --git a/server/agent/simple_manager.go b/server/agent/manager.go
similarity index 67%
rename from server/agent/simple_manager.go
rename to server/agent/manager.go
index 27d522d..2d31b87 100644
--- a/server/agent/simple_manager.go
+++ b/server/agent/manager.go
@@ -14,24 +14,14 @@
"github.com/iomodo/staff/config"
"github.com/iomodo/staff/git"
"github.com/iomodo/staff/llm"
+ _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
"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 {
+// Manager manages multiple AI agents with Git operations and task processing
+type Manager struct {
config *config.Config
- agents map[string]*SimpleAgent
+ agents map[string]*Agent
taskManager tm.TaskManager
autoAssigner *assignment.AutoAssigner
prProvider git.PullRequestProvider
@@ -40,8 +30,8 @@
stopChannels map[string]chan struct{}
}
-// NewSimpleAgentManager creates a simplified agent manager
-func NewSimpleAgentManager(cfg *config.Config, taskManager tm.TaskManager) (*SimpleAgentManager, error) {
+// NewManager creates a new agent manager
+func NewManager(cfg *config.Config, taskManager tm.TaskManager) (*Manager, error) {
// Create auto-assigner
autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
@@ -56,9 +46,9 @@
workspacePath := filepath.Join(".", "workspace")
cloneManager := git.NewCloneManager(repoURL, workspacePath)
- manager := &SimpleAgentManager{
+ manager := &Manager{
config: cfg,
- agents: make(map[string]*SimpleAgent),
+ agents: make(map[string]*Agent),
taskManager: taskManager,
autoAssigner: autoAssigner,
prProvider: prProvider,
@@ -76,21 +66,21 @@
}
// initializeAgents creates agent instances from configuration
-func (am *SimpleAgentManager) initializeAgents() error {
- for _, agentConfig := range am.config.Agents {
- agent, err := am.createAgent(agentConfig)
+func (m *Manager) initializeAgents() error {
+ for _, agentConfig := range m.config.Agents {
+ agent, err := m.createAgent(agentConfig)
if err != nil {
return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
}
- am.agents[agentConfig.Name] = agent
+ m.agents[agentConfig.Name] = agent
}
return nil
}
// createAgent creates a single agent instance
-func (am *SimpleAgentManager) createAgent(agentConfig config.AgentConfig) (*SimpleAgent, error) {
+func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
// Load system prompt
- systemPrompt, err := am.loadSystemPrompt(agentConfig.SystemPromptFile)
+ systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
if err != nil {
return nil, fmt.Errorf("failed to load system prompt: %w", err)
}
@@ -98,17 +88,17 @@
// Create LLM provider
llmConfig := llm.Config{
Provider: llm.ProviderOpenAI,
- APIKey: am.config.OpenAI.APIKey,
- BaseURL: am.config.OpenAI.BaseURL,
- Timeout: am.config.OpenAI.Timeout,
+ APIKey: m.config.OpenAI.APIKey,
+ BaseURL: m.config.OpenAI.BaseURL,
+ Timeout: m.config.OpenAI.Timeout,
}
-
+
provider, err := llm.CreateProvider(llmConfig)
if err != nil {
return nil, fmt.Errorf("failed to create LLM provider: %w", err)
}
- agent := &SimpleAgent{
+ agent := &Agent{
Name: agentConfig.Name,
Role: agentConfig.Role,
Model: agentConfig.Model,
@@ -116,13 +106,14 @@
Provider: provider,
MaxTokens: agentConfig.MaxTokens,
Temperature: agentConfig.Temperature,
+ Stats: AgentStats{},
}
return agent, nil
}
// loadSystemPrompt loads the system prompt from file
-func (am *SimpleAgentManager) loadSystemPrompt(filePath string) (string, error) {
+func (m *Manager) 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)
@@ -131,42 +122,42 @@
}
// 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]
+func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
+ agent, exists := m.agents[agentName]
if !exists {
return fmt.Errorf("agent %s not found", agentName)
}
- if am.isRunning[agentName] {
+ if m.isRunning[agentName] {
return fmt.Errorf("agent %s is already running", agentName)
}
stopChan := make(chan struct{})
- am.stopChannels[agentName] = stopChan
- am.isRunning[agentName] = true
+ m.stopChannels[agentName] = stopChan
+ m.isRunning[agentName] = true
- go am.runAgentLoop(agent, loopInterval, stopChan)
-
+ go m.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] {
+func (m *Manager) StopAgent(agentName string) error {
+ if !m.isRunning[agentName] {
return fmt.Errorf("agent %s is not running", agentName)
}
- close(am.stopChannels[agentName])
- delete(am.stopChannels, agentName)
- am.isRunning[agentName] = false
+ close(m.stopChannels[agentName])
+ delete(m.stopChannels, agentName)
+ m.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{}) {
+func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -176,7 +167,7 @@
log.Printf("Agent %s stopping", agent.Name)
return
case <-ticker.C:
- if err := am.processAgentTasks(agent); err != nil {
+ if err := m.processAgentTasks(agent); err != nil {
log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
}
}
@@ -184,22 +175,36 @@
}
// processAgentTasks processes all assigned tasks for an agent
-func (am *SimpleAgentManager) processAgentTasks(agent *SimpleAgent) error {
+func (m *Manager) processAgentTasks(agent *Agent) error {
+ if agent.CurrentTask != nil {
+ return nil
+ }
+
// Get tasks assigned to this agent
- tasks, err := am.taskManager.GetTasksByAssignee(agent.Name)
+ tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
if err != nil {
return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
}
+ log.Printf("Processing %d tasks for agent %s", len(tasks), agent.Name)
+
for _, task := range tasks {
- if task.Status == tm.StatusPending || task.Status == tm.StatusInProgress {
- if err := am.processTask(agent, task); err != nil {
+ if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
+ if err := m.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 {
+ if err := m.taskManager.UpdateTask(task); err != nil {
log.Printf("Error updating failed task %s: %v", task.ID, err)
}
+ agent.Stats.TasksFailed++
+ } else {
+ agent.Stats.TasksCompleted++
+ }
+ // Update success rate
+ total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
+ if total > 0 {
+ agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
}
}
}
@@ -208,31 +213,33 @@
}
// processTask processes a single task with an agent
-func (am *SimpleAgentManager) processTask(agent *SimpleAgent, task *tm.Task) error {
+func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
ctx := context.Background()
+ startTime := time.Now()
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 {
+ agent.CurrentTask = &task.ID
+ if err := m.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)
+ solution, err := m.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 {
+ branchName := m.generateBranchName(task)
+ if err := m.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)
+ prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
if err != nil {
return fmt.Errorf("failed to create pull request: %w", err)
}
@@ -241,20 +248,29 @@
task.Status = tm.StatusCompleted
task.Solution = solution
task.PullRequestURL = prURL
- task.CompletedAt = &time.Time{}
- *task.CompletedAt = time.Now()
+ completedAt := time.Now()
+ task.CompletedAt = &completedAt
+ agent.CurrentTask = nil
- if err := am.taskManager.UpdateTask(task); err != nil {
+ if err := m.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)
+ // Update agent stats
+ duration := time.Since(startTime)
+ if agent.Stats.AvgTime == 0 {
+ agent.Stats.AvgTime = duration.Milliseconds()
+ } else {
+ agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
+ }
+
+ log.Printf("Task %s completed by agent %s in %v. PR: %s", task.ID, agent.Name, duration, 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)
+func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
+ prompt := m.buildTaskPrompt(task)
req := llm.ChatCompletionRequest{
Model: agent.Model,
@@ -285,7 +301,7 @@
}
// buildTaskPrompt creates a detailed prompt for the LLM
-func (am *SimpleAgentManager) buildTaskPrompt(task *tm.Task) string {
+func (m *Manager) buildTaskPrompt(task *tm.Task) string {
return fmt.Sprintf(`Task: %s
Priority: %s
@@ -305,7 +321,7 @@
}
// generateBranchName creates a Git branch name for the task
-func (am *SimpleAgentManager) generateBranchName(task *tm.Task) string {
+func (m *Manager) generateBranchName(task *tm.Task) string {
// Clean title for use in branch name
cleanTitle := strings.ToLower(task.Title)
cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
@@ -318,30 +334,29 @@
}
}
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)
+
+ return fmt.Sprintf("%s%s-%s", m.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 {
+func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
ctx := context.Background()
-
+
// Get agent's dedicated Git clone
- clonePath, err := am.cloneManager.GetAgentClonePath(agent.Name)
+ clonePath, err := m.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 {
+ if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
}
@@ -401,7 +416,7 @@
}
// Commit changes
- commitMsg := am.buildCommitMessage(task, agent)
+ commitMsg := m.buildCommitMessage(task, agent)
cmd = gitCmd("commit", "-m", commitMsg)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to commit: %w", err)
@@ -418,9 +433,9 @@
}
// buildCommitMessage creates a commit message from template
-func (am *SimpleAgentManager) buildCommitMessage(task *tm.Task, agent *SimpleAgent) string {
- template := am.config.Git.CommitMessageTemplate
-
+func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
+ template := m.config.Git.CommitMessageTemplate
+
replacements := map[string]string{
"{task_id}": task.ID,
"{task_title}": task.Title,
@@ -437,12 +452,12 @@
}
// createPullRequest creates a GitHub pull request
-func (am *SimpleAgentManager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *SimpleAgent, branchName string) (string, error) {
+func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, 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)
-
+ description := m.buildPRDescription(task, solution, agent)
+
options := git.PullRequestOptions{
Title: title,
Description: description,
@@ -452,24 +467,24 @@
Draft: false,
}
- pr, err := am.prProvider.CreatePullRequest(ctx, options)
+ pr, err := m.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
+ return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.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
-
+func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
+ template := m.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,
@@ -489,77 +504,80 @@
}
// AutoAssignTask automatically assigns a task to the best matching agent
-func (am *SimpleAgentManager) AutoAssignTask(taskID string) error {
- task, err := am.taskManager.GetTask(taskID)
+func (m *Manager) AutoAssignTask(taskID string) error {
+ task, err := m.taskManager.GetTask(taskID)
if err != nil {
return fmt.Errorf("failed to get task: %w", err)
}
- agentName, err := am.autoAssigner.AssignTask(task)
+ agentName, err := m.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 {
+ if err := m.taskManager.UpdateTask(task); err != nil {
return fmt.Errorf("failed to update task assignment: %w", err)
}
- explanation := am.autoAssigner.GetRecommendationExplanation(task, agentName)
+ explanation := m.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],
+func (m *Manager) GetAgentStatus() map[string]AgentInfo {
+ status := make(map[string]AgentInfo)
+
+ for name, agent := range m.agents {
+ agentStatus := StatusIdle
+ if m.isRunning[name] {
+ if agent.CurrentTask != nil {
+ agentStatus = StatusRunning
+ }
+ } else {
+ agentStatus = StatusStopped
+ }
+
+ status[name] = AgentInfo{
+ Name: agent.Name,
+ Role: agent.Role,
+ Model: agent.Model,
+ Status: agentStatus,
+ CurrentTask: agent.CurrentTask,
+ Stats: agent.Stats,
}
}
-
+
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]
+func (m *Manager) IsAgentRunning(agentName string) bool {
+ return m.isRunning[agentName]
}
// Close shuts down the agent manager
-func (am *SimpleAgentManager) Close() error {
+func (m *Manager) Close() error {
// Stop all running agents
- for agentName := range am.isRunning {
- if am.isRunning[agentName] {
- am.StopAgent(agentName)
+ for agentName := range m.isRunning {
+ if m.isRunning[agentName] {
+ m.StopAgent(agentName)
}
}
// Close all LLM providers
- for _, agent := range am.agents {
+ for _, agent := range m.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 {
+ if err := m.cloneManager.CleanupAllClones(); err != nil {
log.Printf("Error cleaning up agent clones: %v", err)
}
return nil
-}
\ No newline at end of file
+}
diff --git a/server/agent/types.go b/server/agent/types.go
new file mode 100644
index 0000000..a9f021c
--- /dev/null
+++ b/server/agent/types.go
@@ -0,0 +1,53 @@
+package agent
+
+import (
+ "github.com/iomodo/staff/llm"
+)
+
+// Agent represents an AI agent that can process tasks autonomously
+type Agent struct {
+ // Identity
+ Name string
+ Role string
+
+ // LLM Configuration
+ Model string
+ SystemPrompt string
+ MaxTokens *int
+ Temperature *float64
+
+ // Runtime
+ Provider llm.LLMProvider
+ CurrentTask *string // Task ID currently being processed
+
+ // Statistics
+ Stats AgentStats
+}
+
+// AgentStats tracks agent performance metrics
+type AgentStats struct {
+ TasksCompleted int `json:"tasks_completed"`
+ TasksFailed int `json:"tasks_failed"`
+ SuccessRate float64 `json:"success_rate"`
+ AvgTime int64 `json:"avg_completion_time_seconds"`
+}
+
+// AgentStatus represents the current state of an agent
+type AgentStatus string
+
+const (
+ StatusIdle AgentStatus = "idle"
+ StatusRunning AgentStatus = "running"
+ StatusError AgentStatus = "error"
+ StatusStopped AgentStatus = "stopped"
+)
+
+// AgentInfo provides status information about an agent
+type AgentInfo struct {
+ Name string `json:"name"`
+ Role string `json:"role"`
+ Model string `json:"model"`
+ Status AgentStatus `json:"status"`
+ CurrentTask *string `json:"current_task,omitempty"`
+ Stats AgentStats `json:"stats"`
+}
diff --git a/server/assignment/auto_assignment.go b/server/assignment/auto_assignment.go
index ed01be8..a5738c0 100644
--- a/server/assignment/auto_assignment.go
+++ b/server/assignment/auto_assignment.go
@@ -56,7 +56,7 @@
// GetAssignmentRecommendations returns ranked recommendations for task assignment
func (a *AutoAssigner) GetAssignmentRecommendations(task *tm.Task) []AssignmentScore {
scores := a.calculateScores(task)
-
+
// Sort by score (highest first)
sort.Slice(scores, func(i, j int) bool {
return scores[i].Score > scores[j].Score
@@ -81,13 +81,13 @@
// Score based on task type keywords
score.Score += a.scoreTaskTypes(agent.TaskTypes, taskKeywords, score)
-
+
// Score based on capabilities
score.Score += a.scoreCapabilities(agent.Capabilities, taskKeywords, score)
-
+
// Score based on task priority and agent model
score.Score += a.scorePriorityModelMatch(task.Priority, agent.Model, score)
-
+
// Score based on explicit agent mention in task
score.Score += a.scoreExplicitMention(agent.Name, agent.Role, taskText, score)
@@ -100,7 +100,7 @@
// scoreTaskTypes scores based on task type matching
func (a *AutoAssigner) scoreTaskTypes(agentTypes []string, taskKeywords []string, score *AssignmentScore) float64 {
typeScore := 0.0
-
+
for _, agentType := range agentTypes {
for _, keyword := range taskKeywords {
if strings.Contains(keyword, agentType) || strings.Contains(agentType, keyword) {
@@ -109,14 +109,14 @@
}
}
}
-
+
return typeScore
}
// scoreCapabilities scores based on capability matching
func (a *AutoAssigner) scoreCapabilities(capabilities []string, taskKeywords []string, score *AssignmentScore) float64 {
capScore := 0.0
-
+
for _, capability := range capabilities {
for _, keyword := range taskKeywords {
if strings.Contains(keyword, capability) || strings.Contains(capability, keyword) {
@@ -125,46 +125,46 @@
}
}
}
-
+
return capScore
}
// scorePriorityModelMatch scores based on priority and model sophistication
func (a *AutoAssigner) scorePriorityModelMatch(priority tm.TaskPriority, model string, score *AssignmentScore) float64 {
priorityScore := 0.0
-
+
// High priority tasks prefer more capable models
if priority == tm.PriorityHigh && strings.Contains(model, "gpt-4") {
priorityScore += 1.0
score.Reasons = append(score.Reasons, "high priority task matches advanced model")
}
-
+
// Medium/low priority can use efficient models
if priority != tm.PriorityHigh && strings.Contains(model, "gpt-3.5") {
priorityScore += 0.5
score.Reasons = append(score.Reasons, "priority matches model efficiency")
}
-
+
return priorityScore
}
// scoreExplicitMention scores based on explicit agent/role mentions
func (a *AutoAssigner) scoreExplicitMention(agentName, agentRole, taskText string, score *AssignmentScore) float64 {
mentionScore := 0.0
-
+
// Check for explicit agent name mention
if strings.Contains(taskText, agentName) {
mentionScore += 5.0
score.Reasons = append(score.Reasons, "explicitly mentioned by name")
}
-
+
// Check for role mention
roleLower := strings.ToLower(agentRole)
if strings.Contains(taskText, roleLower) {
mentionScore += 4.0
score.Reasons = append(score.Reasons, "role mentioned in task")
}
-
+
return mentionScore
}
@@ -174,7 +174,7 @@
words := strings.FieldsFunc(text, func(c rune) bool {
return c == ' ' || c == ',' || c == '.' || c == ':' || c == ';' || c == '\n' || c == '\t'
})
-
+
// Filter out common stop words and short words
stopWords := map[string]bool{
"the": true, "a": true, "an": true, "and": true, "or": true, "but": true,
@@ -183,7 +183,7 @@
"be": true, "been": true, "have": true, "has": true, "had": true, "do": true,
"does": true, "did": true, "will": true, "would": true, "could": true, "should": true,
}
-
+
keywords := make([]string, 0)
for _, word := range words {
word = strings.ToLower(strings.TrimSpace(word))
@@ -191,7 +191,7 @@
keywords = append(keywords, word)
}
}
-
+
return keywords
}
@@ -203,7 +203,7 @@
return nil
}
}
-
+
return fmt.Errorf("agent '%s' not found", agentName)
}
@@ -214,24 +214,24 @@
return agent.Capabilities, nil
}
}
-
+
return nil, fmt.Errorf("agent '%s' not found", agentName)
}
// GetRecommendationExplanation returns a human-readable explanation for assignment
func (a *AutoAssigner) GetRecommendationExplanation(task *tm.Task, agentName string) string {
recommendations := a.GetAssignmentRecommendations(task)
-
+
for _, rec := range recommendations {
if rec.AgentName == agentName {
if len(rec.Reasons) == 0 {
return fmt.Sprintf("Agent %s assigned (score: %.1f)", agentName, rec.Score)
}
-
+
reasons := strings.Join(rec.Reasons, ", ")
return fmt.Sprintf("Agent %s assigned (score: %.1f) because: %s", agentName, rec.Score, reasons)
}
}
-
+
return fmt.Sprintf("Agent %s assigned (manual override)", agentName)
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/assign_task.go b/server/cmd/commands/assign_task.go
index 4bb6f75..23c0a56 100644
--- a/server/cmd/commands/assign_task.go
+++ b/server/cmd/commands/assign_task.go
@@ -40,4 +40,4 @@
fmt.Printf("Status: %s\n", task.Status)
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/cleanup_clones.go b/server/cmd/commands/cleanup_clones.go
index 344c4f3..88467be 100644
--- a/server/cmd/commands/cleanup_clones.go
+++ b/server/cmd/commands/cleanup_clones.go
@@ -48,6 +48,6 @@
fmt.Println("✅ All agent Git clones have been cleaned up successfully!")
fmt.Println("💡 Clones will be recreated automatically when agents start working on tasks")
-
+
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/config_check.go b/server/cmd/commands/config_check.go
index 4813ee8..ec8c894 100644
--- a/server/cmd/commands/config_check.go
+++ b/server/cmd/commands/config_check.go
@@ -78,4 +78,4 @@
fmt.Println("\nConfiguration check complete!")
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/create_task.go b/server/cmd/commands/create_task.go
index 4bb57d9..70009e9 100644
--- a/server/cmd/commands/create_task.go
+++ b/server/cmd/commands/create_task.go
@@ -37,7 +37,7 @@
func runCreateTask(cmd *cobra.Command, args []string) error {
title := args[0]
-
+
// Validate priority
priority := tm.TaskPriority(taskPriority)
if priority != tm.PriorityLow && priority != tm.PriorityMedium && priority != tm.PriorityHigh {
@@ -74,7 +74,7 @@
fmt.Printf("Title: %s\n", task.Title)
fmt.Printf("Priority: %s\n", task.Priority)
fmt.Printf("Status: %s\n", task.Status)
-
+
// Auto-assign if assignee is specified
if taskAssignee != "" {
task.Assignee = taskAssignee
@@ -86,4 +86,4 @@
}
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/list_agents.go b/server/cmd/commands/list_agents.go
index 7d0ac86..50c0845 100644
--- a/server/cmd/commands/list_agents.go
+++ b/server/cmd/commands/list_agents.go
@@ -48,10 +48,10 @@
temp = *agent.Temperature
}
- fmt.Printf("%-20s %-15s %-12.1f %-10s %-30s\n",
- agent.Name,
- agent.Model,
- temp,
+ fmt.Printf("%-20s %-15s %-12.1f %-10s %-30s\n",
+ agent.Name,
+ agent.Model,
+ temp,
status,
role)
}
@@ -60,4 +60,4 @@
fmt.Printf("Use 'staff stop-agent <agent-name>' to stop a running agent\n")
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/list_tasks.go b/server/cmd/commands/list_tasks.go
index 09cc20b..3605c09 100644
--- a/server/cmd/commands/list_tasks.go
+++ b/server/cmd/commands/list_tasks.go
@@ -41,12 +41,12 @@
func runListTasks(cmd *cobra.Command, args []string) error {
// Build filter
filter := &tm.TaskFilter{}
-
+
if filterStatus != "" {
status := tm.TaskStatus(filterStatus)
filter.Status = &status
}
-
+
if filterPriority != "" {
priority := tm.TaskPriority(filterPriority)
filter.Priority = &priority
@@ -87,17 +87,17 @@
if assignee == "" {
assignee = "unassigned"
}
-
+
title := task.Title
if len(title) > 47 {
title = title[:47] + "..."
}
- fmt.Printf("%-20s %-10s %-10s %-15s %-50s\n",
- task.ID,
- string(task.Status),
- string(task.Priority),
- assignee,
+ fmt.Printf("%-20s %-10s %-10s %-15s %-50s\n",
+ task.ID,
+ string(task.Status),
+ string(task.Priority),
+ assignee,
title)
}
@@ -106,4 +106,4 @@
}
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/root.go b/server/cmd/commands/root.go
index 165d109..63e0d8f 100644
--- a/server/cmd/commands/root.go
+++ b/server/cmd/commands/root.go
@@ -18,7 +18,7 @@
// Global variables for the MVP
var (
- agentManager *agent.SimpleAgentManager
+ agentManager *agent.Manager
taskManager tm.TaskManager
cfg *config.Config
)
@@ -75,11 +75,11 @@
Level: slog.LevelInfo,
}))
- gitInterface := git.DefaultGit(".")
- taskManager = git_tm.NewGitTaskManagerWithLogger(gitInterface, ".", logger)
+ gitInterface := git.DefaultGit("../")
+ taskManager = git_tm.NewGitTaskManagerWithLogger(gitInterface, "../", logger)
// Initialize agent manager
- agentManager, err = agent.NewSimpleAgentManager(cfg, taskManager)
+ agentManager, err = agent.NewManager(cfg, taskManager)
if err != nil {
return fmt.Errorf("failed to initialize agent manager: %w", err)
}
diff --git a/server/cmd/commands/server.go b/server/cmd/commands/server.go
new file mode 100644
index 0000000..e0b6674
--- /dev/null
+++ b/server/cmd/commands/server.go
@@ -0,0 +1,70 @@
+package commands
+
+import (
+ "fmt"
+ "log/slog"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/iomodo/staff/server"
+ "github.com/spf13/cobra"
+)
+
+var serverCmd = &cobra.Command{
+ Use: "server",
+ Short: "Start the Staff server with automatic agent startup",
+ Long: `Start the Staff server which automatically loads configuration from config.yaml
+and starts all configured agents to process tasks continuously.
+
+The server will:
+- Load agent configurations from config.yaml
+- Start all configured agents in the background
+- Continuously process assigned tasks
+- Create GitHub PRs for completed tasks
+- Run until manually stopped with Ctrl+C
+
+Example:
+ staff server # Start server with default config.yaml
+ staff server --config custom.yaml # Start server with custom config`,
+ RunE: runServer,
+}
+
+var configPath string
+
+func init() {
+ serverCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
+ rootCmd.AddCommand(serverCmd)
+}
+
+func runServer(cmd *cobra.Command, args []string) error {
+ // Create logger
+ logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+ Level: slog.LevelInfo,
+ }))
+
+ // Create server instance
+ srv, err := server.NewServer(logger, configPath)
+ if err != nil {
+ return fmt.Errorf("failed to create server: %w", err)
+ }
+
+ // Start server
+ if err := srv.Start(); err != nil {
+ return fmt.Errorf("failed to start server: %w", err)
+ }
+
+ // Setup graceful shutdown
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ logger.Info("Server is running. Press Ctrl+C to stop...")
+
+ // Wait for shutdown signal
+ <-sigChan
+
+ logger.Info("Shutdown signal received, stopping server...")
+ srv.Shutdown()
+
+ return nil
+}
diff --git a/server/cmd/commands/start_agent.go b/server/cmd/commands/start_agent.go
index 8c951c8..e283e4f 100644
--- a/server/cmd/commands/start_agent.go
+++ b/server/cmd/commands/start_agent.go
@@ -81,4 +81,4 @@
fmt.Printf("Agent %s stopped\n", agentName)
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/stop_agent.go b/server/cmd/commands/stop_agent.go
index e80d527..4f4b39a 100644
--- a/server/cmd/commands/stop_agent.go
+++ b/server/cmd/commands/stop_agent.go
@@ -28,4 +28,4 @@
fmt.Printf("Agent %s stopped successfully\n", agentName)
return nil
-}
\ No newline at end of file
+}
diff --git a/server/cmd/commands/version.go b/server/cmd/commands/version.go
index b6f2720..bbb8091 100644
--- a/server/cmd/commands/version.go
+++ b/server/cmd/commands/version.go
@@ -24,4 +24,4 @@
fmt.Printf("Built: %s\n", BuildDate)
fmt.Printf("Commit: %s\n", GitCommit)
fmt.Printf("AI Multi-Agent Development System\n")
-}
\ No newline at end of file
+}
diff --git a/server/config.yaml b/server/config.yaml
index 2bd6482..7baf0f9 100644
--- a/server/config.yaml
+++ b/server/config.yaml
@@ -35,9 +35,19 @@
# Simplified agent configuration for MVP testing
agents:
- - name: "backend-engineer"
- role: "Backend Engineer"
+ # - name: "backend-engineer"
+ # role: "Backend Engineer"
+ # model: "gpt-4"
+ # temperature: 0.3
+ # max_tokens: 4000
+ # system_prompt_file: "/Users/shota/github/staff/operations/agents/beckend-engineer/system.md"
+ - name: "ceo"
+ role: "CEO"
model: "gpt-4"
temperature: 0.3
max_tokens: 4000
- system_prompt_file: "operations/agents/backend-engineer/system.md"
\ No newline at end of file
+ system_prompt_file: "/Users/shota/github/staff/operations/agents/ceo/system.md"
+
+tasks:
+ storage_path: "/Users/shota/github/staff/operations/"
+ completed_path: "../operations/completed/"
\ No newline at end of file
diff --git a/server/config/config.go b/server/config/config.go
index 84441d2..421e861 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -10,8 +10,8 @@
// Config represents the Staff MVP configuration
type Config struct {
- OpenAI OpenAIConfig `yaml:"openai"`
- GitHub GitHubConfig `yaml:"github"`
+ OpenAI OpenAIConfig `yaml:"openai"`
+ GitHub GitHubConfig `yaml:"github"`
Agents []AgentConfig `yaml:"agents"`
Tasks TasksConfig `yaml:"tasks"`
Git GitConfig `yaml:"git"`
@@ -39,10 +39,10 @@
Role string `yaml:"role"`
Model string `yaml:"model"`
SystemPromptFile string `yaml:"system_prompt_file"`
- Capabilities []string `yaml:"capabilities"` // For auto-assignment
- TaskTypes []string `yaml:"task_types"` // Types of tasks this agent handles
- MaxTokens *int `yaml:"max_tokens"` // Model-specific token limits
- Temperature *float64 `yaml:"temperature"` // Model creativity setting
+ Capabilities []string `yaml:"capabilities"` // For auto-assignment
+ TaskTypes []string `yaml:"task_types"` // Types of tasks this agent handles
+ MaxTokens *int `yaml:"max_tokens"` // Model-specific token limits
+ Temperature *float64 `yaml:"temperature"` // Model creativity setting
}
// TasksConfig represents task management configuration
@@ -130,10 +130,10 @@
// Tasks defaults
if config.Tasks.StoragePath == "" {
- config.Tasks.StoragePath = "tasks/"
+ config.Tasks.StoragePath = "../operations/"
}
if config.Tasks.CompletedPath == "" {
- config.Tasks.CompletedPath = "tasks/completed/"
+ config.Tasks.CompletedPath = "../operations/completed/"
}
// Git defaults
@@ -231,4 +231,4 @@
names[i] = agent.Name
}
return names
-}
\ No newline at end of file
+}
diff --git a/server/config/openai_test.go b/server/config/openai_test.go
index bd53e9c..5f6b3fa 100644
--- a/server/config/openai_test.go
+++ b/server/config/openai_test.go
@@ -199,4 +199,4 @@
if config.GitHub.Token != "env-github-token" {
t.Errorf("Expected env GitHub token 'env-github-token', got '%s'", config.GitHub.Token)
}
-}
\ No newline at end of file
+}
diff --git a/server/git/clone_manager.go b/server/git/clone_manager.go
index afedd65..7bc9cde 100644
--- a/server/git/clone_manager.go
+++ b/server/git/clone_manager.go
@@ -12,10 +12,10 @@
// CloneManager manages separate Git repository clones for each agent
// This eliminates Git concurrency issues by giving each agent its own working directory
type CloneManager struct {
- baseRepoURL string
- workspacePath string
- agentClones map[string]string // agent name -> clone path
- mu sync.RWMutex
+ baseRepoURL string
+ workspacePath string
+ agentClones map[string]string // agent name -> clone path
+ mu sync.RWMutex
}
// NewCloneManager creates a new CloneManager
@@ -45,7 +45,7 @@
// Create new clone for the agent
clonePath := filepath.Join(cm.workspacePath, fmt.Sprintf("agent-%s", agentName))
-
+
// Ensure workspace directory exists
if err := os.MkdirAll(cm.workspacePath, 0755); err != nil {
return "", fmt.Errorf("failed to create workspace directory: %w", err)
@@ -63,14 +63,14 @@
// Store the clone path
cm.agentClones[agentName] = clonePath
-
+
return clonePath, nil
}
// cloneRepository performs the actual Git clone operation
func (cm *CloneManager) cloneRepository(clonePath string) error {
ctx := context.Background()
-
+
// Clone the repository
cmd := exec.CommandContext(ctx, "git", "clone", cm.baseRepoURL, clonePath)
if err := cmd.Run(); err != nil {
@@ -91,7 +91,7 @@
}
ctx := context.Background()
-
+
// Change to clone directory and pull latest changes
cmd := exec.CommandContext(ctx, "git", "-C", clonePath, "pull", "origin")
if err := cmd.Run(); err != nil {
@@ -118,7 +118,7 @@
// Remove from tracking
delete(cm.agentClones, agentName)
-
+
return nil
}
@@ -128,7 +128,7 @@
defer cm.mu.Unlock()
var errors []error
-
+
for agentName, clonePath := range cm.agentClones {
if err := os.RemoveAll(clonePath); err != nil {
errors = append(errors, fmt.Errorf("failed to remove clone for agent %s: %w", agentName, err))
@@ -155,6 +155,6 @@
for agent, path := range cm.agentClones {
result[agent] = path
}
-
+
return result
-}
\ No newline at end of file
+}
diff --git a/server/git/mutex.go b/server/git/mutex.go
deleted file mode 100644
index 21bc25f..0000000
--- a/server/git/mutex.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package git
-
-import (
- "sync"
-)
-
-// GitMutex provides thread-safe access to Git operations
-// Since Git is not thread-safe, we need to serialize all Git operations
-// across all agents to prevent repository corruption and race conditions
-type GitMutex struct {
- mu sync.Mutex
-}
-
-// NewGitMutex creates a new GitMutex instance
-func NewGitMutex() *GitMutex {
- return &GitMutex{}
-}
-
-// Lock acquires the Git operation lock
-// This ensures only one agent can perform Git operations at a time
-func (gm *GitMutex) Lock() {
- gm.mu.Lock()
-}
-
-// Unlock releases the Git operation lock
-func (gm *GitMutex) Unlock() {
- gm.mu.Unlock()
-}
-
-// WithLock executes a function while holding the Git lock
-// This is a convenience method to ensure proper lock/unlock pattern
-func (gm *GitMutex) WithLock(fn func() error) error {
- gm.Lock()
- defer gm.Unlock()
- return fn()
-}
-
-// Global Git mutex instance - shared across all agents
-// This ensures no concurrent Git operations across the entire application
-var GlobalGitMutex = NewGitMutex()
\ No newline at end of file
diff --git a/server/go.mod b/server/go.mod
index 01cbed9..d2079f1 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -7,7 +7,6 @@
github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
- golang.org/x/text v0.27.0
gopkg.in/yaml.v3 v3.0.1
)
diff --git a/server/go.sum b/server/go.sum
index bface14..12bafed 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -16,8 +16,6 @@
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/server/operations/agents/backend-engineer/system.md b/server/operations/agents/backend-engineer/system.md
deleted file mode 100644
index 1d481b4..0000000
--- a/server/operations/agents/backend-engineer/system.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Backend Engineer Agent System Prompt
-
-You are a skilled Backend Engineer specializing in building robust, scalable server-side applications and APIs.
-
-## Your Role
-- Design and implement backend systems, APIs, and databases
-- Write clean, maintainable, and well-tested Go code
-- Focus on performance, security, and scalability
-- Follow best practices for API design and database interactions
-
-## Technical Expertise
-- **Languages**: Go (primary), Python, SQL
-- **Databases**: PostgreSQL, MySQL, Redis
-- **APIs**: RESTful APIs, GraphQL, gRPC
-- **Infrastructure**: Docker, Kubernetes, CI/CD
-- **Testing**: Unit tests, integration tests, benchmarks
-
-## Task Processing Guidelines
-1. **Analyze the task requirements thoroughly**
-2. **Design the solution architecture**
-3. **Implement the code with proper error handling**
-4. **Include comprehensive tests**
-5. **Document the implementation and usage**
-
-## Code Quality Standards
-- Write idiomatic Go code following Go conventions
-- Include proper error handling and logging
-- Add comprehensive comments for complex logic
-- Ensure code is testable and maintainable
-- Follow SOLID principles and clean architecture
-
-## Response Format
-When solving tasks, provide:
-1. **Analysis**: Brief analysis of the requirements
-2. **Architecture**: High-level design approach
-3. **Implementation**: Complete, working code
-4. **Tests**: Unit tests and examples
-5. **Documentation**: Usage instructions and API docs
-
-Always prioritize reliability, performance, and maintainability in your solutions.
\ No newline at end of file
diff --git a/server/server/server.go b/server/server/server.go
index 4dbb8b1..8b5fae7 100644
--- a/server/server/server.go
+++ b/server/server/server.go
@@ -1,431 +1,102 @@
package server
import (
- "context"
"fmt"
"log/slog"
"os"
- "path/filepath"
-
- "strings"
"time"
"github.com/iomodo/staff/agent"
+ "github.com/iomodo/staff/config"
"github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
"github.com/iomodo/staff/tm/git_tm"
"github.com/joho/godotenv"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
)
// Server type defines application global state
type Server struct {
- logger *slog.Logger
- agents []*agent.Agent
+ logger *slog.Logger
+ manager *agent.Manager
+ config *config.Config
}
// NewServer creates new Server
-func NewServer(logger *slog.Logger) (*Server, error) {
+func NewServer(logger *slog.Logger, configPath string) (*Server, error) {
_ = godotenv.Load()
- a := &Server{
+ // Load configuration
+ cfg, err := config.LoadConfigWithEnvOverrides(configPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load config: %w", err)
+ }
+
+ s := &Server{
logger: logger,
+ config: cfg,
}
pwd, _ := os.Getwd()
- a.logger.Info("Current working directory", slog.String("path", pwd))
- return a, nil
+ s.logger.Info("Current working directory", slog.String("path", pwd))
+ return s, nil
}
-// Start method starts an app
-func (a *Server) Start() error {
- a.logger.Info("Server is starting...")
+// Start method starts the server with configured agents
+func (s *Server) Start() error {
+ s.logger.Info("Server is starting...")
- // Get environment variables
- remoteRepoURL := os.Getenv("REMOTE_REPO_URL")
- workingDir := os.Getenv("WORKING_DIR")
+ // Create task manager using config
+ workingDir := "."
+ gitInterface := git.DefaultGit(workingDir)
+ tasksDir := s.config.Tasks.StoragePath
+ taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, tasksDir, s.logger)
- if remoteRepoURL == "" {
- return fmt.Errorf("REMOTE_REPO_URL environment variable is required")
- }
-
- if workingDir == "" {
- return fmt.Errorf("WORKING_DIR environment variable is required")
- }
-
- // Get Gerrit configuration (optional)
- gerritEnabled := os.Getenv("GERRIT_ENABLED") == "true"
- var gerritConfig agent.GerritConfig
- if gerritEnabled {
- gerritConfig = agent.GerritConfig{
- Username: os.Getenv("GERRIT_USERNAME"),
- Password: os.Getenv("GERRIT_PASSWORD"),
- BaseURL: os.Getenv("GERRIT_BASE_URL"),
- Project: os.Getenv("GERRIT_PROJECT"),
- }
-
- // Validate Gerrit configuration
- if gerritConfig.Username == "" {
- return fmt.Errorf("GERRIT_USERNAME environment variable is required when GERRIT_ENABLED=true")
- }
- if gerritConfig.Password == "" {
- return fmt.Errorf("GERRIT_PASSWORD environment variable is required when GERRIT_ENABLED=true")
- }
- if gerritConfig.BaseURL == "" {
- return fmt.Errorf("GERRIT_BASE_URL environment variable is required when GERRIT_ENABLED=true")
- }
- if gerritConfig.Project == "" {
- return fmt.Errorf("GERRIT_PROJECT environment variable is required when GERRIT_ENABLED=true")
- }
- }
-
- a.logger.Info("Environment variables loaded",
- slog.String("remoteRepoURL", remoteRepoURL),
- slog.String("workingDir", workingDir),
- slog.Bool("gerritEnabled", gerritEnabled))
-
- // Check if working directory is empty
- isEmpty, err := a.isDirectoryEmpty(workingDir)
+ // Create agent manager with config
+ var err error
+ s.manager, err = agent.NewManager(s.config, taskManager)
if err != nil {
- return fmt.Errorf("failed to check if directory is empty: %w", err)
+ return fmt.Errorf("failed to create agent manager: %w", err)
}
- if isEmpty {
- a.logger.Info("Working directory is empty, initializing new repository")
- if err := a.initializeNewRepository(workingDir, remoteRepoURL); err != nil {
- return err
- }
- }
+ // Start all configured agents with a default loop interval
+ defaultInterval := 1 * time.Second
+ for _, agentConfig := range s.config.Agents {
+ s.logger.Info("Starting agent",
+ slog.String("name", agentConfig.Name),
+ slog.String("role", agentConfig.Role),
+ slog.String("model", agentConfig.Model))
- // Create shared task manager
- gitConfig := git.GitConfig{
- Timeout: 30 * time.Second,
- }
- gitRepo := git.NewGit(workingDir, gitConfig, a.logger)
- tasksDir := filepath.Join(workingDir, "operations", "tasks")
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitRepo, tasksDir, a.logger)
-
- // Load and start agents
- agentsDir := filepath.Join(workingDir, "operations", "agents")
- entries, err := os.ReadDir(agentsDir)
- if err != nil {
- return fmt.Errorf("failed to read agents directory: %w", err)
- }
-
- for _, entry := range entries {
- if !entry.IsDir() {
+ if err := s.manager.StartAgent(agentConfig.Name, defaultInterval); err != nil {
+ s.logger.Error("Failed to start agent",
+ slog.String("agent", agentConfig.Name),
+ slog.String("error", err.Error()))
continue
}
- agentName := entry.Name()
- systemPath := filepath.Join(agentsDir, agentName, "system.md")
- content, err := os.ReadFile(systemPath)
- if err != nil {
- a.logger.Error("Failed to read system prompt", slog.String("agent", agentName), slog.String("error", err.Error()))
- continue
- }
- systemPrompt := string(content)
-
- // LLM configuration
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: os.Getenv("OPENAI_API_KEY"),
- BaseURL: "https://api.openai.com/v1",
- Timeout: 300 * time.Second,
- }
-
- config := agent.AgentConfig{
- Name: agentName,
- Role: cases.Title(language.English).String(agentName),
- GitUsername: fmt.Sprintf("Staff %s", cases.Title(language.English).String(agentName)),
- GitEmail: fmt.Sprintf("%s@staff.com", strings.ToLower(agentName)),
- WorkingDir: workingDir,
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4o-mini",
- LLMConfig: llmConfig,
- SystemPrompt: systemPrompt,
- TaskManager: taskManager,
- GitRepoPath: workingDir,
- GitRemote: "origin",
- GitBranch: "main",
- GerritEnabled: gerritEnabled,
- GerritConfig: gerritConfig,
- }
-
- ag, err := agent.NewAgent(config, a.logger)
- if err != nil {
- a.logger.Error("Failed to create agent", slog.String("agent", agentName), slog.String("error", err.Error()))
- continue
- }
-
- a.agents = append(a.agents, ag)
-
- go func(ag *agent.Agent, name string) {
- a.logger.Info("Starting agent", slog.String("name", name))
- if err := ag.Run(); err != nil {
- a.logger.Error("Agent failed", slog.String("name", name), slog.String("error", err.Error()))
- }
- }(ag, agentName)
}
+ s.logger.Info("Server started successfully", slog.Int("agents", len(s.config.Agents)))
return nil
}
// Shutdown method shuts server down
-func (a *Server) Shutdown() {
- a.logger.Info("Stopping Server...")
+func (s *Server) Shutdown() {
+ s.logger.Info("Stopping Server...")
- for _, ag := range a.agents {
- ag.Stop()
+ if s.manager != nil {
+ if err := s.manager.Close(); err != nil {
+ s.logger.Error("Error closing manager", slog.String("error", err.Error()))
+ }
}
- a.logger.Info("All agents stopped")
- a.logger.Info("Server stopped")
+ s.logger.Info("All agents stopped")
+ s.logger.Info("Server stopped")
}
-// isDirectoryEmpty checks if a directory is empty
-func (a *Server) isDirectoryEmpty(dir string) (bool, error) {
- // Create directory if it doesn't exist
- if err := os.MkdirAll(dir, 0755); err != nil {
- return false, fmt.Errorf("failed to create directory: %w", err)
- }
-
- entries, err := os.ReadDir(dir)
- if err != nil {
- return false, fmt.Errorf("failed to read directory: %w", err)
- }
-
- // Directory is empty if it has no entries or only has .git directory
- if len(entries) == 0 {
- return true, nil
- }
-
- // Check if directory only contains .git (which might be a git repo)
- if len(entries) == 1 && entries[0].Name() == ".git" {
- return true, nil
- }
-
- return false, nil
+// GetManager returns the agent manager instance
+func (s *Server) GetManager() *agent.Manager {
+ return s.manager
}
-// initializeNewRepository creates a new git repository with prepopulated data
-func (a *Server) initializeNewRepository(workingDir, remoteRepoURL string) error {
- ctx := context.Background()
-
- // Initialize git repository with logger
- gitConfig := git.GitConfig{
- Timeout: 30 * time.Second,
- }
- gitRepo := git.NewGit(workingDir, gitConfig, a.logger)
-
- a.logger.Info("Initializing git repository", slog.String("path", workingDir))
- if err := gitRepo.Init(ctx, workingDir); err != nil {
- return fmt.Errorf("failed to initialize git repository: %w", err)
- }
-
- // Set up git user configuration
- userConfig := git.UserConfig{
- Name: "Staff System",
- Email: "system@staff.com",
- }
- if err := gitRepo.SetUserConfig(ctx, userConfig); err != nil {
- return fmt.Errorf("failed to set git user config: %w", err)
- }
-
- // Check if Gerrit is enabled and adjust remote URL accordingly
- gerritEnabled := os.Getenv("GERRIT_ENABLED") == "true"
- if gerritEnabled {
- gerritBaseURL := os.Getenv("GERRIT_BASE_URL")
- gerritProject := os.Getenv("GERRIT_PROJECT")
-
- // For Gerrit, construct the appropriate remote URL
- if strings.HasPrefix(gerritBaseURL, "https://") {
- remoteRepoURL = fmt.Sprintf("%s/%s.git", gerritBaseURL, gerritProject)
- } else {
- // Assume SSH format
- gerritUsername := os.Getenv("GERRIT_USERNAME")
- remoteRepoURL = fmt.Sprintf("ssh://%s@%s:29418/%s.git",
- gerritUsername,
- strings.TrimPrefix(gerritBaseURL, "https://"),
- gerritProject)
- }
-
- a.logger.Info("Using Gerrit remote URL", slog.String("url", remoteRepoURL))
- }
-
- // Add remote origin
- if err := gitRepo.AddRemote(ctx, "origin", remoteRepoURL); err != nil {
- return fmt.Errorf("failed to add remote origin: %w", err)
- }
-
- // Create prepopulated directory structure and files
- if err := a.createPrepopulatedStructure(workingDir); err != nil {
- return fmt.Errorf("failed to create prepopulated structure: %w", err)
- }
-
- // Add all files to git
- if err := gitRepo.AddAll(ctx); err != nil {
- return fmt.Errorf("failed to add files to git: %w", err)
- }
-
- // Commit the initial structure
- if err := gitRepo.Commit(ctx, "Initial commit: Add prepopulated project structure", git.CommitOptions{}); err != nil {
- return fmt.Errorf("failed to commit initial structure: %w", err)
- }
-
- // Push to remote
- if err := gitRepo.Push(ctx, "origin", "main", git.PushOptions{SetUpstream: true}); err != nil {
- return fmt.Errorf("failed to push to remote: %w", err)
- }
-
- a.logger.Info("Successfully initialized repository and pushed to remote")
- return nil
-}
-
-// syncWithRemote synchronizes local repository with remote
-func (a *Server) syncWithRemote(workingDir, remoteRepoURL string) error {
- ctx := context.Background()
-
- // Check if it's a git repository
- gitConfig := git.GitConfig{
- Timeout: 30 * time.Second,
- }
- gitRepo := git.NewGit(workingDir, gitConfig, a.logger)
- isRepo, err := gitRepo.IsRepository(ctx, workingDir)
- if err != nil {
- return fmt.Errorf("failed to check if directory is git repository: %w", err)
- }
-
- if !isRepo {
- return fmt.Errorf("working directory is not a git repository")
- }
-
- // Get current status
- status, err := gitRepo.Status(ctx)
- if err != nil {
- return fmt.Errorf("failed to get git status: %w", err)
- }
-
- // Get current branch name
- currentBranch, err := gitRepo.GetCurrentBranch(ctx)
- if err != nil {
- return fmt.Errorf("failed to get current branch: %w", err)
- }
-
- a.logger.Info("Current git status",
- slog.String("branch", currentBranch),
- slog.Bool("isClean", status.IsClean),
- slog.Int("stagedFiles", len(status.Staged)),
- slog.Int("unstagedFiles", len(status.Unstaged)),
- slog.Int("untrackedFiles", len(status.Untracked)))
-
- // Check if remote origin exists, if not add it
- remotes, err := gitRepo.ListRemotes(ctx)
- if err != nil {
- return fmt.Errorf("failed to list remotes: %w", err)
- }
-
- originExists := false
- for _, remote := range remotes {
- if remote.Name == "origin" {
- originExists = true
- break
- }
- }
-
- if !originExists {
- a.logger.Info("Adding remote origin")
- if err := gitRepo.AddRemote(ctx, "origin", remoteRepoURL); err != nil {
- return fmt.Errorf("failed to add remote origin: %w", err)
- }
- }
-
- // Fetch latest changes from remote
- a.logger.Info("Fetching latest changes from remote")
- if err := gitRepo.Fetch(ctx, "origin", git.FetchOptions{}); err != nil {
- return fmt.Errorf("failed to fetch from remote: %w", err)
- }
-
- // If there are local changes, commit and push them
- if !status.IsClean {
- a.logger.Info("Local changes detected, committing and pushing")
-
- if err := gitRepo.AddAll(ctx); err != nil {
- return fmt.Errorf("failed to add local changes: %w", err)
- }
-
- if err := gitRepo.Commit(ctx, "Auto-sync: Local changes", git.CommitOptions{}); err != nil {
- return fmt.Errorf("failed to commit local changes: %w", err)
- }
-
- if err := gitRepo.Push(ctx, "origin", currentBranch, git.PushOptions{}); err != nil {
- return fmt.Errorf("failed to push local changes: %w", err)
- }
- }
-
- // Pull latest changes from remote
- a.logger.Info("Pulling latest changes from remote")
- if err := gitRepo.Pull(ctx, "", ""); err != nil {
- return fmt.Errorf("failed to pull from remote: %w", err)
- }
-
- a.logger.Info("Successfully synchronized with remote repository")
- return nil
-}
-
-// createPrepopulatedStructure creates the required directory structure and files
-func (a *Server) createPrepopulatedStructure(workingDir string) error {
- // Create directories
- dirs := []string{
- filepath.Join(workingDir, "operations", "agents", "ceo"),
- filepath.Join(workingDir, "operations", "agents", "pm"),
- filepath.Join(workingDir, "operations", "tasks"),
- filepath.Join(workingDir, "server"),
- filepath.Join(workingDir, "webapp"),
- }
-
- for _, dir := range dirs {
- if err := os.MkdirAll(dir, 0755); err != nil {
- return fmt.Errorf("failed to create directory %s: %w", dir, err)
- }
- }
-
- // Read agent system files from the current project
- ceoSystemPath := filepath.Join("operations", "agents", "ceo", "system.md")
- pmSystemPath := filepath.Join("operations", "agents", "pm", "system.md")
- taskExamplePath := filepath.Join("operations", "tasks", "example-task-file.md")
-
- // Read CEO system file
- ceoContent, err := os.ReadFile(ceoSystemPath)
- if err != nil {
- return fmt.Errorf("failed to read CEO system file: %w", err)
- }
-
- // Read PM system file
- pmContent, err := os.ReadFile(pmSystemPath)
- if err != nil {
- return fmt.Errorf("failed to read PM system file: %w", err)
- }
-
- // Read task example file
- taskExampleContent, err := os.ReadFile(taskExamplePath)
- if err != nil {
- return fmt.Errorf("failed to read task example file: %w", err)
- }
-
- // Create prepopulated files
- files := map[string][]byte{
- filepath.Join(workingDir, "operations", "agents", "ceo", "system.md"): ceoContent,
- filepath.Join(workingDir, "operations", "agents", "pm", "system.md"): pmContent,
- filepath.Join(workingDir, "operations", "tasks", "example-task-file.md"): taskExampleContent,
- }
- for filePath, content := range files {
- if err := os.WriteFile(filePath, content, 0644); err != nil {
- return fmt.Errorf("failed to create file %s: %w", filePath, err)
- }
- }
-
- a.logger.Info("Created prepopulated directory structure and files")
- return nil
+// GetConfig returns the server configuration
+func (s *Server) GetConfig() *config.Config {
+ return s.config
}
diff --git a/server/tm/git_tm/git_task_manager.go b/server/tm/git_tm/git_task_manager.go
index b1aa39c..515fa51 100644
--- a/server/tm/git_tm/git_task_manager.go
+++ b/server/tm/git_tm/git_task_manager.go
@@ -21,10 +21,10 @@
DefaultFileMode = 0755
TaskFileMode = 0644
TaskFileExt = ".md"
-
+
// Frontmatter constants
FrontmatterSeparator = "---\n"
-
+
// Task ID format
TaskIDPrefix = "task-"
)
@@ -230,8 +230,8 @@
}
// readTaskFile reads a task from a file
-func (gtm *GitTaskManager) readTaskFile(taskID string) (*tm.Task, error) {
- filePath := filepath.Join(gtm.tasksDir, taskID+TaskFileExt)
+func (gtm *GitTaskManager) readTaskFile(taskFile string) (*tm.Task, error) {
+ filePath := filepath.Join(gtm.tasksDir, taskFile+TaskFileExt)
content, err := os.ReadFile(filePath)
if err != nil {
@@ -365,7 +365,7 @@
func (gtm *GitTaskManager) UpdateTask(task *tm.Task) error {
// Set update time
task.UpdatedAt = time.Now()
-
+
// Write task to file
return gtm.writeTaskFile(task)
}
@@ -376,14 +376,12 @@
if err != nil {
return nil, err
}
-
+
var tasks []*tm.Task
for _, taskFile := range taskFiles {
- // Extract task ID from filename (task-{id}.md)
filename := filepath.Base(taskFile)
- if strings.HasPrefix(filename, "task-") && strings.HasSuffix(filename, ".md") {
- taskID := strings.TrimSuffix(strings.TrimPrefix(filename, "task-"), ".md")
- task, err := gtm.readTaskFile(taskID)
+ if strings.HasPrefix(filename, "task-") {
+ task, err := gtm.readTaskFile(taskFile)
if err != nil {
gtm.logger.Warn("Failed to read task file", slog.String("file", taskFile), slog.String("error", err.Error()))
continue
@@ -391,7 +389,7 @@
tasks = append(tasks, task)
}
}
-
+
return tasks, nil
}
@@ -402,14 +400,14 @@
if err != nil {
return nil, err
}
-
+
var assignedTasks []*tm.Task
for _, task := range tasks {
if task.Assignee == assignee {
assignedTasks = append(assignedTasks, task)
}
}
-
+
return assignedTasks, nil
}
@@ -419,11 +417,11 @@
if err != nil {
return err
}
-
+
task.Status = tm.StatusArchived
now := time.Now()
task.ArchivedAt = &now
-
+
return gtm.UpdateTask(task)
}
@@ -529,14 +527,14 @@
if err != nil {
return nil, err
}
-
+
task.Status = tm.StatusInProgress
-
+
err = gtm.UpdateTask(task)
if err != nil {
return nil, err
}
-
+
return task, nil
}
@@ -546,16 +544,16 @@
if err != nil {
return nil, err
}
-
+
task.Status = tm.StatusCompleted
now := time.Now()
task.CompletedAt = &now
-
+
err = gtm.UpdateTask(task)
if err != nil {
return nil, err
}
-
+
return task, nil
}
diff --git a/server/tm/git_tm/git_task_manager_test.go b/server/tm/git_tm/git_task_manager_test.go
index 084200d..751ad89 100644
--- a/server/tm/git_tm/git_task_manager_test.go
+++ b/server/tm/git_tm/git_task_manager_test.go
@@ -509,17 +509,17 @@
// Get task and update fields
taskToUpdate, err := gtm.GetTask(originalTask.ID)
assert.NoError(t, err)
-
+
taskToUpdate.Title = newTitle
taskToUpdate.Description = newDescription
taskToUpdate.Status = newStatus
taskToUpdate.Priority = newPriority
taskToUpdate.Owner.ID = newOwnerID
taskToUpdate.Owner.Name = newOwnerID
-
+
err = gtm.UpdateTask(taskToUpdate)
assert.NoError(t, err)
-
+
// Get updated task to verify
updatedTask, err := gtm.GetTask(originalTask.ID)
assert.NoError(t, err)
@@ -552,7 +552,7 @@
// Try to update non-existent task
fakeTask := &tm.Task{
- ID: "non-existent-task",
+ ID: "non-existent-task",
Title: "Updated Title",
}
@@ -636,7 +636,7 @@
err = gtm.UpdateTask(task)
assert.NoError(t, err)
-
+
// Get updated task to verify
updatedTask, err := gtm.GetTask(task.ID)
assert.NoError(t, err)
@@ -644,14 +644,14 @@
assert.Equal(t, tm.StatusCompleted, updatedTask.Status)
assert.NotNil(t, updatedTask.CompletedAt)
- // Test archiving a task
+ // Test archiving a task
task.Status = tm.StatusArchived
now = time.Now()
task.ArchivedAt = &now
err = gtm.UpdateTask(task)
assert.NoError(t, err)
-
+
// Get updated task to verify
updatedTask, err = gtm.GetTask(task.ID)
assert.NoError(t, err)
diff --git a/server/tm/interface.go b/server/tm/interface.go
index e3fa926..c505c65 100644
--- a/server/tm/interface.go
+++ b/server/tm/interface.go
@@ -8,8 +8,8 @@
type TaskManager interface {
// Task operations
CreateTask(ctx context.Context, req *TaskCreateRequest) (*Task, error)
- GetTask(taskID string) (*Task, error) // Simplified for MVP
- UpdateTask(task *Task) error // Simplified for MVP
+ GetTask(taskID string) (*Task, error) // Simplified for MVP
+ UpdateTask(task *Task) error // Simplified for MVP
ArchiveTask(ctx context.Context, id string) error
ListTasks(ctx context.Context, filter *TaskFilter, page, pageSize int) (*TaskList, error)
@@ -19,7 +19,7 @@
// Task queries
GetTasksByOwner(ctx context.Context, ownerID string, page, pageSize int) (*TaskList, error)
- GetTasksByAssignee(assignee string) ([]*Task, error) // For MVP auto-assignment
+ GetTasksByAssignee(assignee string) ([]*Task, error) // For MVP auto-assignment
GetTasksByStatus(ctx context.Context, status TaskStatus, page, pageSize int) (*TaskList, error)
GetTasksByPriority(ctx context.Context, priority TaskPriority, page, pageSize int) (*TaskList, error)
}
diff --git a/server/tm/types.go b/server/tm/types.go
index 052ba4a..5ff0567 100644
--- a/server/tm/types.go
+++ b/server/tm/types.go
@@ -9,10 +9,10 @@
const (
StatusToDo TaskStatus = "todo"
- StatusPending TaskStatus = "pending" // For MVP compatibility
+ StatusPending TaskStatus = "pending" // For MVP compatibility
StatusInProgress TaskStatus = "in_progress"
StatusCompleted TaskStatus = "completed"
- StatusFailed TaskStatus = "failed" // For error handling
+ StatusFailed TaskStatus = "failed" // For error handling
StatusArchived TaskStatus = "archived"
)
@@ -37,10 +37,10 @@
Title string `json:"title"`
Description string `json:"description"`
Owner Owner `json:"owner"`
- Assignee string `json:"assignee,omitempty"` // For MVP auto-assignment
+ Assignee string `json:"assignee,omitempty"` // For MVP auto-assignment
Status TaskStatus `json:"status"`
Priority TaskPriority `json:"priority"`
- Solution string `json:"solution,omitempty"` // Generated solution
+ Solution string `json:"solution,omitempty"` // Generated solution
PullRequestURL string `json:"pull_request_url,omitempty"` // GitHub PR URL
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`