task: task-1753636924-a1d4c708 - created

Change-Id: Ic78528c47ae38114b9b7504f1c4a76f95e93eb13
diff --git a/server/cmd/commands/assign_task.go b/server/cmd/commands/assign_task.go
new file mode 100644
index 0000000..4bb6f75
--- /dev/null
+++ b/server/cmd/commands/assign_task.go
@@ -0,0 +1,43 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var assignTaskCmd = &cobra.Command{
+	Use:   "assign-task [task-id] [agent-name]",
+	Short: "Assign a task to an agent",
+	Long: `Assign an existing task to a specific agent.
+
+Examples:
+  staff assign-task task-1234567890-abcd1234 backend-engineer
+  staff assign-task task-1234567890-abcd1234 frontend-engineer`,
+	Args: cobra.ExactArgs(2),
+	RunE: runAssignTask,
+}
+
+func runAssignTask(cmd *cobra.Command, args []string) error {
+	taskID := args[0]
+	agentName := args[1]
+
+	// Get the task
+	task, err := taskManager.GetTask(taskID)
+	if err != nil {
+		return fmt.Errorf("failed to get task: %w", err)
+	}
+
+	// Assign the task
+	task.Assignee = agentName
+	if err := taskManager.UpdateTask(task); err != nil {
+		return fmt.Errorf("failed to assign task: %w", err)
+	}
+
+	fmt.Printf("Task %s assigned to %s successfully!\n", taskID, agentName)
+	fmt.Printf("Title: %s\n", task.Title)
+	fmt.Printf("Priority: %s\n", task.Priority)
+	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
new file mode 100644
index 0000000..344c4f3
--- /dev/null
+++ b/server/cmd/commands/cleanup_clones.go
@@ -0,0 +1,53 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var cleanupClonesCmd = &cobra.Command{
+	Use:   "cleanup-clones",
+	Short: "Clean up all agent Git clones",
+	Long: `Remove all agent Git clone directories to free up disk space.
+
+This command will:
+- Stop any running agents
+- Remove all agent-specific Git clone directories
+- Free up disk space used by clones
+
+Examples:
+  staff cleanup-clones`,
+	RunE: runCleanupClones,
+}
+
+// Note: Command is added in root.go init() function
+
+func runCleanupClones(cmd *cobra.Command, args []string) error {
+	if agentManager == nil {
+		return fmt.Errorf("agent manager not initialized")
+	}
+
+	// Stop all running agents first
+	fmt.Println("Stopping all running agents...")
+	for _, agent := range cfg.Agents {
+		if agentManager.IsAgentRunning(agent.Name) {
+			if err := agentManager.StopAgent(agent.Name); err != nil {
+				fmt.Printf("Warning: Failed to stop agent %s: %v\n", agent.Name, err)
+			} else {
+				fmt.Printf("Stopped agent: %s\n", agent.Name)
+			}
+		}
+	}
+
+	// Cleanup all clones by closing the agent manager
+	// This will trigger the cleanup automatically
+	if err := agentManager.Close(); err != nil {
+		return fmt.Errorf("failed to cleanup agent clones: %w", err)
+	}
+
+	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
new file mode 100644
index 0000000..4813ee8
--- /dev/null
+++ b/server/cmd/commands/config_check.go
@@ -0,0 +1,81 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var configCheckCmd = &cobra.Command{
+	Use:   "config-check",
+	Short: "Check configuration validity",
+	Long: `Check the current configuration for errors and display settings.
+
+Examples:
+  staff config-check`,
+	RunE: runConfigCheck,
+}
+
+func runConfigCheck(cmd *cobra.Command, args []string) error {
+	fmt.Println("Configuration Check:")
+	fmt.Println("==================")
+
+	// Check OpenAI configuration
+	if cfg.OpenAI.APIKey == "" {
+		fmt.Println("❌ OpenAI API key is missing")
+	} else {
+		fmt.Printf("✅ OpenAI API key configured (ends with: ...%s)\n", cfg.OpenAI.APIKey[len(cfg.OpenAI.APIKey)-4:])
+	}
+
+	if cfg.OpenAI.BaseURL == "" {
+		fmt.Println("â„šī¸  OpenAI Base URL using default")
+	} else {
+		fmt.Printf("â„šī¸  OpenAI Base URL: %s\n", cfg.OpenAI.BaseURL)
+	}
+
+	// Check GitHub configuration
+	if cfg.GitHub.Token == "" {
+		fmt.Println("❌ GitHub token is missing")
+	} else {
+		fmt.Printf("✅ GitHub token configured (ends with: ...%s)\n", cfg.GitHub.Token[len(cfg.GitHub.Token)-4:])
+	}
+
+	if cfg.GitHub.Owner == "" {
+		fmt.Println("❌ GitHub owner is missing")
+	} else {
+		fmt.Printf("✅ GitHub owner: %s\n", cfg.GitHub.Owner)
+	}
+
+	if cfg.GitHub.Repo == "" {
+		fmt.Println("❌ GitHub repo is missing")
+	} else {
+		fmt.Printf("✅ GitHub repo: %s\n", cfg.GitHub.Repo)
+	}
+
+	// Check agents configuration
+	fmt.Printf("\nAgents: %d configured\n", len(cfg.Agents))
+	for i, agent := range cfg.Agents {
+		temp := 0.7
+		if agent.Temperature != nil {
+			temp = *agent.Temperature
+		}
+		fmt.Printf("  %d. %s (model: %s, temp: %.1f)\n", i+1, agent.Name, agent.Model, temp)
+	}
+
+	// Check task manager
+	if taskManager == nil {
+		fmt.Println("❌ Task manager not initialized")
+	} else {
+		fmt.Println("✅ Task manager initialized")
+	}
+
+	// Check agent manager
+	if agentManager == nil {
+		fmt.Println("❌ Agent manager not initialized")
+	} else {
+		fmt.Println("✅ Agent manager initialized")
+	}
+
+	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
new file mode 100644
index 0000000..4bb57d9
--- /dev/null
+++ b/server/cmd/commands/create_task.go
@@ -0,0 +1,89 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/iomodo/staff/tm"
+	"github.com/spf13/cobra"
+)
+
+var createTaskCmd = &cobra.Command{
+	Use:   "create-task [title]",
+	Short: "Create a new task",
+	Long: `Create a new task with specified title, description, and priority.
+
+Examples:
+  staff create-task "Add user authentication"
+  staff create-task "Fix login bug" --description "Users cannot log in with Google OAuth" --priority high --assignee backend-engineer`,
+	Args: cobra.ExactArgs(1),
+	RunE: runCreateTask,
+}
+
+var (
+	taskDescription string
+	taskPriority    string
+	taskAssignee    string
+	taskDueDate     string
+)
+
+func init() {
+	createTaskCmd.Flags().StringVarP(&taskDescription, "description", "d", "", "Task description")
+	createTaskCmd.Flags().StringVarP(&taskPriority, "priority", "p", "medium", "Task priority (low, medium, high)")
+	createTaskCmd.Flags().StringVarP(&taskAssignee, "assignee", "a", "", "Agent to assign the task to")
+	createTaskCmd.Flags().StringVar(&taskDueDate, "due", "", "Due date (RFC3339 format, e.g., 2024-01-15T10:00:00Z)")
+}
+
+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 {
+		return fmt.Errorf("invalid priority: %s (must be low, medium, or high)", taskPriority)
+	}
+
+	// Parse due date if provided
+	var dueDate *time.Time
+	if taskDueDate != "" {
+		parsed, err := time.Parse(time.RFC3339, taskDueDate)
+		if err != nil {
+			return fmt.Errorf("invalid due date format: %s (expected RFC3339)", taskDueDate)
+		}
+		dueDate = &parsed
+	}
+
+	// Create task request
+	req := &tm.TaskCreateRequest{
+		Title:       title,
+		Description: taskDescription,
+		OwnerID:     "user", // MVP: single user
+		Priority:    priority,
+		DueDate:     dueDate,
+	}
+
+	// Create the task
+	task, err := taskManager.CreateTask(context.Background(), req)
+	if err != nil {
+		return fmt.Errorf("failed to create task: %w", err)
+	}
+
+	fmt.Printf("Task created successfully!\n")
+	fmt.Printf("ID: %s\n", task.ID)
+	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
+		if err := taskManager.UpdateTask(task); err != nil {
+			fmt.Printf("Warning: Failed to assign task to %s: %v\n", taskAssignee, err)
+		} else {
+			fmt.Printf("Assigned to: %s\n", taskAssignee)
+		}
+	}
+
+	return nil
+}
\ No newline at end of file
diff --git a/server/cmd/commands/list_agents.go b/server/cmd/commands/list_agents.go
new file mode 100644
index 0000000..7d0ac86
--- /dev/null
+++ b/server/cmd/commands/list_agents.go
@@ -0,0 +1,63 @@
+package commands
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+var listAgentsCmd = &cobra.Command{
+	Use:   "list-agents",
+	Short: "List all configured agents",
+	Long: `List all configured agents with their settings and status.
+
+Examples:
+  staff list-agents`,
+	RunE: runListAgents,
+}
+
+func runListAgents(cmd *cobra.Command, args []string) error {
+	if len(cfg.Agents) == 0 {
+		fmt.Println("No agents configured")
+		return nil
+	}
+
+	fmt.Printf("Found %d configured agents:\n\n", len(cfg.Agents))
+
+	// Display agents in table format
+	fmt.Printf("%-20s %-15s %-12s %-10s %-30s\n", "Name", "Model", "Temperature", "Status", "Role/Description")
+	fmt.Printf("%s\n", strings.Repeat("-", 90))
+
+	for _, agent := range cfg.Agents {
+		status := "stopped"
+		if agentManager.IsAgentRunning(agent.Name) {
+			status = "running"
+		}
+
+		role := agent.Role
+		if role == "" {
+			role = "general"
+		}
+		if len(role) > 27 {
+			role = role[:27] + "..."
+		}
+
+		temp := 0.7
+		if agent.Temperature != nil {
+			temp = *agent.Temperature
+		}
+
+		fmt.Printf("%-20s %-15s %-12.1f %-10s %-30s\n", 
+			agent.Name, 
+			agent.Model, 
+			temp, 
+			status,
+			role)
+	}
+
+	fmt.Printf("\nUse 'staff start-agent <agent-name>' to start an agent\n")
+	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
new file mode 100644
index 0000000..09cc20b
--- /dev/null
+++ b/server/cmd/commands/list_tasks.go
@@ -0,0 +1,109 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/iomodo/staff/tm"
+	"github.com/spf13/cobra"
+)
+
+var listTasksCmd = &cobra.Command{
+	Use:   "list-tasks",
+	Short: "List all tasks",
+	Long: `List all tasks with optional filtering by status, priority, or assignee.
+
+Examples:
+  staff list-tasks
+  staff list-tasks --status todo
+  staff list-tasks --priority high
+  staff list-tasks --assignee backend-engineer`,
+	RunE: runListTasks,
+}
+
+var (
+	filterStatus   string
+	filterPriority string
+	filterAssignee string
+	pageSize       int = 20
+	pageNum        int = 0
+)
+
+func init() {
+	listTasksCmd.Flags().StringVar(&filterStatus, "status", "", "Filter by status (todo, in_progress, completed, archived)")
+	listTasksCmd.Flags().StringVar(&filterPriority, "priority", "", "Filter by priority (low, medium, high)")
+	listTasksCmd.Flags().StringVar(&filterAssignee, "assignee", "", "Filter by assignee")
+	listTasksCmd.Flags().IntVar(&pageSize, "page-size", 20, "Number of tasks per page")
+	listTasksCmd.Flags().IntVar(&pageNum, "page", 0, "Page number (0-based)")
+}
+
+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
+	}
+
+	// Get tasks
+	taskList, err := taskManager.ListTasks(context.Background(), filter, pageNum, pageSize)
+	if err != nil {
+		return fmt.Errorf("failed to list tasks: %w", err)
+	}
+
+	// Filter by assignee if specified (not in TaskFilter interface yet)
+	var filteredTasks []*tm.Task
+	if filterAssignee != "" {
+		for _, task := range taskList.Tasks {
+			if task.Assignee == filterAssignee {
+				filteredTasks = append(filteredTasks, task)
+			}
+		}
+	} else {
+		filteredTasks = taskList.Tasks
+	}
+
+	// Display results
+	if len(filteredTasks) == 0 {
+		fmt.Println("No tasks found")
+		return nil
+	}
+
+	fmt.Printf("Found %d tasks (page %d/%d)\n\n", len(filteredTasks), pageNum+1, (taskList.TotalCount+pageSize-1)/pageSize)
+
+	// Display tasks in table format
+	fmt.Printf("%-20s %-10s %-10s %-15s %-50s\n", "ID", "Status", "Priority", "Assignee", "Title")
+	fmt.Printf("%s\n", strings.Repeat("-", 110))
+
+	for _, task := range filteredTasks {
+		assignee := task.Assignee
+		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, 
+			title)
+	}
+
+	if taskList.HasMore {
+		fmt.Printf("\nUse --page %d to see more tasks\n", pageNum+1)
+	}
+
+	return nil
+}
\ No newline at end of file
diff --git a/server/cmd/commands/root.go b/server/cmd/commands/root.go
index 088e7ff..165d109 100644
--- a/server/cmd/commands/root.go
+++ b/server/cmd/commands/root.go
@@ -1,62 +1,88 @@
 package commands
 
 import (
+	"fmt"
 	"log/slog"
 	"os"
-	"os/signal"
-	"syscall"
 
-	"github.com/iomodo/staff/server"
+	"github.com/iomodo/staff/agent"
+	"github.com/iomodo/staff/config"
+	"github.com/iomodo/staff/git"
+	"github.com/iomodo/staff/tm"
+	"github.com/iomodo/staff/tm/git_tm"
 	"github.com/spf13/cobra"
 )
 
 // Command is an abstraction of the cobra Command
 type Command = cobra.Command
 
+// Global variables for the MVP
+var (
+	agentManager *agent.SimpleAgentManager
+	taskManager  tm.TaskManager
+	cfg          *config.Config
+)
+
 // Run function starts the application
 func Run(args []string) error {
 	rootCmd.SetArgs(args)
 	return rootCmd.Execute()
 }
 
-// rootCmd is a command to run the server.
+// rootCmd is the main command for Staff MVP
 var rootCmd = &cobra.Command{
-	Use:   "server",
-	Short: "Runs a server",
-	Long:  `Runs a server. Killing the process will stop the server`,
-	RunE:  serverCmdF,
+	Use:   "staff",
+	Short: "Staff - AI Multi-Agent Development System",
+	Long: `Staff MVP - AI agents that autonomously handle development tasks and create GitHub PRs.
+
+Examples:
+  staff create-task "Add user authentication" --priority high --agent backend-engineer
+  staff start-agent backend-engineer
+  staff list-tasks
+  staff list-agents`,
+	PersistentPreRunE: initializeApp,
 }
 
-func serverCmdF(_ *cobra.Command, _ []string) error {
-	srv, err := runServer()
-	if err != nil {
-		return err
+func init() {
+	// Add all commands
+	rootCmd.AddCommand(createTaskCmd)
+	rootCmd.AddCommand(assignTaskCmd)
+	rootCmd.AddCommand(startAgentCmd)
+	rootCmd.AddCommand(stopAgentCmd)
+	rootCmd.AddCommand(listTasksCmd)
+	rootCmd.AddCommand(listAgentsCmd)
+	rootCmd.AddCommand(configCheckCmd)
+	rootCmd.AddCommand(cleanupClonesCmd)
+	rootCmd.AddCommand(versionCmd)
+}
+
+// initializeApp loads configuration and sets up managers
+func initializeApp(cmd *cobra.Command, args []string) error {
+	// Skip initialization for help and version commands
+	if cmd.Name() == "help" || cmd.Name() == "version" {
+		return nil
 	}
-	defer srv.Shutdown()
 
-	// wait for kill signal before attempting to gracefully shutdown
-	// the running service
-	interruptChan := make(chan os.Signal, 1)
-	signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-	<-interruptChan
-	return nil
-}
+	// Load configuration
+	var err error
+	cfg, err = config.LoadConfigWithEnvOverrides("config.yaml")
+	if err != nil {
+		return fmt.Errorf("failed to load config: %w", err)
+	}
 
-func runServer() (*server.Server, error) {
+	// Initialize task manager
 	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
-		Level: slog.LevelDebug,
+		Level: slog.LevelInfo,
 	}))
 
-	srv, err := server.NewServer(logger)
+	gitInterface := git.DefaultGit(".")
+	taskManager = git_tm.NewGitTaskManagerWithLogger(gitInterface, ".", logger)
+
+	// Initialize agent manager
+	agentManager, err = agent.NewSimpleAgentManager(cfg, taskManager)
 	if err != nil {
-		logger.Error(err.Error())
-		return nil, err
+		return fmt.Errorf("failed to initialize agent manager: %w", err)
 	}
 
-	serverErr := srv.Start()
-	if serverErr != nil {
-		logger.Error(serverErr.Error())
-		return nil, serverErr
-	}
-	return srv, nil
+	return nil
 }
diff --git a/server/cmd/commands/start_agent.go b/server/cmd/commands/start_agent.go
new file mode 100644
index 0000000..8c951c8
--- /dev/null
+++ b/server/cmd/commands/start_agent.go
@@ -0,0 +1,84 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var startAgentCmd = &cobra.Command{
+	Use:   "start-agent [agent-name]",
+	Short: "Start an agent to process tasks",
+	Long: `Start a specific agent to continuously process assigned tasks.
+
+The agent will:
+1. Check for assigned tasks every 30 seconds
+2. Process tasks using the configured LLM
+3. Create GitHub PRs for solutions
+4. Mark tasks as completed
+
+Examples:
+  staff start-agent backend-engineer
+  staff start-agent frontend-engineer`,
+	Args: cobra.ExactArgs(1),
+	RunE: runStartAgent,
+}
+
+var (
+	agentInterval time.Duration = 30 * time.Second
+)
+
+func init() {
+	startAgentCmd.Flags().DurationVar(&agentInterval, "interval", 30*time.Second, "Task check interval")
+}
+
+func runStartAgent(cmd *cobra.Command, args []string) error {
+	agentName := args[0]
+
+	// Check if agent exists in configuration
+	var agentExists bool
+	for _, agent := range cfg.Agents {
+		if agent.Name == agentName {
+			agentExists = true
+			break
+		}
+	}
+
+	if !agentExists {
+		return fmt.Errorf("agent '%s' not found in configuration", agentName)
+	}
+
+	fmt.Printf("Starting agent: %s\n", agentName)
+	fmt.Printf("Task check interval: %v\n", agentInterval)
+	fmt.Printf("Press Ctrl+C to stop the agent\n\n")
+
+	// Set up signal handling for graceful shutdown
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+	go func() {
+		<-sigChan
+		fmt.Printf("\nReceived shutdown signal, stopping agent...\n")
+		cancel()
+	}()
+
+	// Start the agent
+	err := agentManager.StartAgent(agentName, agentInterval)
+	if err != nil {
+		return fmt.Errorf("failed to start agent: %w", err)
+	}
+
+	// Wait for context cancellation (Ctrl+C)
+	<-ctx.Done()
+
+	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
new file mode 100644
index 0000000..e80d527
--- /dev/null
+++ b/server/cmd/commands/stop_agent.go
@@ -0,0 +1,31 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var stopAgentCmd = &cobra.Command{
+	Use:   "stop-agent [agent-name]",
+	Short: "Stop a running agent",
+	Long: `Stop a specific running agent.
+
+Examples:
+  staff stop-agent backend-engineer
+  staff stop-agent frontend-engineer`,
+	Args: cobra.ExactArgs(1),
+	RunE: runStopAgent,
+}
+
+func runStopAgent(cmd *cobra.Command, args []string) error {
+	agentName := args[0]
+
+	err := agentManager.StopAgent(agentName)
+	if err != nil {
+		return fmt.Errorf("failed to stop agent: %w", err)
+	}
+
+	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
new file mode 100644
index 0000000..b6f2720
--- /dev/null
+++ b/server/cmd/commands/version.go
@@ -0,0 +1,27 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+const (
+	Version   = "0.1.0"
+	BuildDate = "2024-01-15"
+	GitCommit = "mvp-build"
+)
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Show version information",
+	Long:  `Display the current version of Staff MVP.`,
+	Run:   runVersion,
+}
+
+func runVersion(cmd *cobra.Command, args []string) {
+	fmt.Printf("Staff MVP v%s\n", Version)
+	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