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