Refactor everything
Change-Id: Ic3a37c38cfecba943c91f6ae545ce1c5b551c0d5
diff --git a/server/tm/git_tm/git_task_manager.go b/server/tm/git_tm/git_task_manager.go
index 515fa51..cca9da8 100644
--- a/server/tm/git_tm/git_task_manager.go
+++ b/server/tm/git_tm/git_task_manager.go
@@ -5,12 +5,14 @@
"fmt"
"log/slog"
"os"
+ "os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/google/uuid"
+ "github.com/iomodo/staff/config"
"github.com/iomodo/staff/git"
"github.com/iomodo/staff/tm"
"gopkg.in/yaml.v3"
@@ -29,68 +31,23 @@
TaskIDPrefix = "task-"
)
-// UserService defines interface for user-related operations
-type UserService interface {
- GetUserName(userID string) (string, error)
-}
-
-// DefaultUserService provides a simple implementation that uses userID as name
-type DefaultUserService struct{}
-
-func (dus *DefaultUserService) GetUserName(userID string) (string, error) {
- // For now, just return the userID as the name
- // This can be enhanced to lookup from a proper user service
- return userID, nil
-}
-
// GitTaskManager implements TaskManager interface using git as the source of truth
type GitTaskManager struct {
- git git.GitInterface
- repoPath string
- tasksDir string
- logger *slog.Logger
- userService UserService
+ git git.GitInterface
+ repoPath string
+ tasksDir string
+ config *config.Config
+ logger *slog.Logger
}
// NewGitTaskManager creates a new GitTaskManager instance
-func NewGitTaskManager(git git.GitInterface, repoPath string, logger *slog.Logger) *GitTaskManager {
+func NewGitTaskManager(gitInter git.GitInterface, cfg *config.Config, logger *slog.Logger) *GitTaskManager {
return &GitTaskManager{
- git: git,
- repoPath: repoPath,
- tasksDir: filepath.Join(repoPath, "tasks"),
- logger: logger,
- userService: &DefaultUserService{},
- }
-}
-
-// NewGitTaskManagerWithLogger creates a new GitTaskManager instance with a custom logger
-func NewGitTaskManagerWithLogger(git git.GitInterface, repoPath string, logger *slog.Logger) *GitTaskManager {
- if logger == nil {
- logger = slog.Default()
- }
- return &GitTaskManager{
- git: git,
- repoPath: repoPath,
- tasksDir: filepath.Join(repoPath, "tasks"),
- logger: logger,
- userService: &DefaultUserService{},
- }
-}
-
-// NewGitTaskManagerWithUserService creates a new GitTaskManager with custom user service
-func NewGitTaskManagerWithUserService(git git.GitInterface, repoPath string, logger *slog.Logger, userService UserService) *GitTaskManager {
- if logger == nil {
- logger = slog.Default()
- }
- if userService == nil {
- userService = &DefaultUserService{}
- }
- return &GitTaskManager{
- git: git,
- repoPath: repoPath,
- tasksDir: filepath.Join(repoPath, "tasks"),
- logger: logger,
- userService: userService,
+ git: gitInter,
+ repoPath: cfg.Tasks.StoragePath,
+ tasksDir: filepath.Join(cfg.Tasks.StoragePath, "tasks"),
+ config: cfg,
+ logger: logger,
}
}
@@ -320,12 +277,7 @@
taskID := gtm.generateTaskID()
now := time.Now()
- // Get owner name from user service
- ownerName, err := gtm.userService.GetUserName(req.OwnerID)
- if err != nil {
- gtm.logger.Warn("Failed to get owner name, using ID", slog.String("ownerID", req.OwnerID), slog.String("error", err.Error()))
- ownerName = req.OwnerID
- }
+ ownerName := (req.OwnerID) //TODO: Get owner name from user service
// Create task
task := &tm.Task{
@@ -581,5 +533,600 @@
return gtm.ListTasks(ctx, filter, page, pageSize)
}
+// GenerateSubtaskPR creates a PR with the proposed subtasks
+func (gtm *GitTaskManager) ProposeSubTasks(ctx context.Context, task *tm.Task, analysis *tm.SubtaskAnalysis) (string, error) {
+ branchName := generateBranchName("subtasks", task)
+ gtm.logger.Info("Creating subtask PR", slog.String("branch", branchName))
+
+ // Create Git branch and commit subtask proposal
+ if err := gtm.createSubtaskBranch(ctx, analysis, branchName); err != nil {
+ return "", fmt.Errorf("failed to create subtask branch: %w", err)
+ }
+
+ // Generate PR content
+ prContent := gtm.generateSubtaskPRContent(analysis)
+ title := fmt.Sprintf("Subtask Proposal: %s", analysis.ParentTaskID)
+
+ // Validate PR content
+ if title == "" {
+ return "", fmt.Errorf("PR title cannot be empty")
+ }
+ if prContent == "" {
+ return "", fmt.Errorf("PR description cannot be empty")
+ }
+
+ // Determine base branch (try main first, fallback to master)
+ baseBranch := gtm.determineBaseBranch(ctx)
+ gtm.logger.Info("Using base branch", slog.String("base_branch", baseBranch))
+
+ // Create the pull request
+ options := git.PullRequestOptions{
+ Title: title,
+ Description: prContent,
+ HeadBranch: branchName,
+ BaseBranch: baseBranch,
+ Labels: []string{"subtasks", "proposal", "ai-generated"},
+ Draft: false,
+ }
+
+ gtm.logger.Info("Creating PR with options",
+ slog.String("title", options.Title),
+ slog.String("head_branch", options.HeadBranch),
+ slog.String("base_branch", options.BaseBranch))
+
+ pr, err := gtm.git.CreatePullRequest(ctx, options)
+ if err != nil {
+ return "", fmt.Errorf("failed to create PR: %w", err)
+ }
+
+ gtm.logger.Info("Generated subtask proposal PR", slog.String("pr_url", pr.URL))
+
+ return pr.URL, nil
+}
+
+func (gtm *GitTaskManager) ProposeSolution(ctx context.Context, task *tm.Task, solution, agentName string) (string, error) {
+ branchName := generateBranchName("solution", task)
+ gtm.logger.Info("Creating solution PR", slog.String("branch", branchName))
+
+ if err := gtm.createSolutionBranch(ctx, task, solution, branchName, agentName); err != nil {
+ return "", fmt.Errorf("failed to create solution branch: %w", err)
+ }
+ // Build PR description from template
+ description := buildSolutionPRDescription(task, solution, gtm.config.Git.PRTemplate, agentName)
+
+ options := git.PullRequestOptions{
+ Title: fmt.Sprintf("Task %s: %s", task.ID, task.Title),
+ Description: description,
+ HeadBranch: branchName,
+ BaseBranch: "main",
+ Labels: []string{"ai-generated"},
+ Draft: false,
+ }
+
+ pr, err := gtm.git.CreatePullRequest(ctx, options)
+ if err != nil {
+ return "", fmt.Errorf("failed to create PR: %w", err)
+ }
+ gtm.logger.Info("Generated subtask proposal PR", slog.String("pr_url", pr.URL))
+ return pr.URL, nil
+}
+
+// createSubtaskBranch creates a Git branch with subtask proposal content
+func (gtm *GitTaskManager) createSubtaskBranch(ctx context.Context, analysis *tm.SubtaskAnalysis, branchName string) error {
+ clonePath, err := gtm.git.GetAgentClonePath("subtask-service")
+ if err != nil {
+ return fmt.Errorf("failed to get clone path: %w", err)
+ }
+
+ // All Git operations use the clone directory
+ gitCmd := func(args ...string) *exec.Cmd {
+ return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
+ }
+
+ // Ensure we're on main branch before creating new branch
+ cmd := gitCmd("checkout", "main")
+ if err := cmd.Run(); err != nil {
+ // Try master branch if main doesn't exist
+ cmd = gitCmd("checkout", "master")
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to checkout main/master branch: %w", err)
+ }
+ }
+
+ // Pull latest changes
+ cmd = gitCmd("pull", "origin")
+ if err := cmd.Run(); err != nil {
+ gtm.logger.Warn("Failed to pull latest changes", slog.String("error", err.Error()))
+ }
+
+ // Delete branch if it exists (cleanup from previous attempts)
+ cmd = gitCmd("branch", "-D", branchName)
+ _ = cmd.Run() // Ignore error if branch doesn't exist
+
+ // Also delete remote tracking branch if it exists
+ cmd = gitCmd("push", "origin", "--delete", branchName)
+ _ = cmd.Run() // Ignore error if branch doesn't exist
+
+ // Create and checkout new branch
+ cmd = gitCmd("checkout", "-b", branchName)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to create branch: %w", err)
+ }
+
+ // Create individual task files for each subtask
+ tasksDir := filepath.Join(clonePath, "operations", "tasks")
+ if err := os.MkdirAll(tasksDir, 0755); err != nil {
+ return fmt.Errorf("failed to create tasks directory: %w", err)
+ }
+
+ var stagedFiles []string
+
+ // Update parent task to mark as completed
+ parentTaskFile := filepath.Join(tasksDir, fmt.Sprintf("%s.md", analysis.ParentTaskID))
+ if err := gtm.updateParentTaskAsCompleted(parentTaskFile, analysis); err != nil {
+ return fmt.Errorf("failed to update parent task: %w", err)
+ }
+
+ // Track parent task file for staging
+ parentRelativeFile := filepath.Join("operations", "tasks", fmt.Sprintf("%s.md", analysis.ParentTaskID))
+ stagedFiles = append(stagedFiles, parentRelativeFile)
+ gtm.logger.Info("Updated parent task file", slog.String("file", parentRelativeFile))
+
+ // Create a file for each subtask
+ for i, subtask := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ taskFile := filepath.Join(tasksDir, fmt.Sprintf("%s.md", taskID))
+ taskContent := gtm.generateSubtaskFile(subtask, taskID, analysis.ParentTaskID)
+
+ if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil {
+ return fmt.Errorf("failed to write subtask file %s: %w", taskID, err)
+ }
+
+ // Track file for staging
+ relativeFile := filepath.Join("operations", "tasks", fmt.Sprintf("%s.md", taskID))
+ stagedFiles = append(stagedFiles, relativeFile)
+ gtm.logger.Info("Created subtask file", slog.String("file", relativeFile))
+ }
+
+ // Stage all subtask files
+ for _, file := range stagedFiles {
+ cmd = gitCmd("add", file)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to stage file %s: %w", file, err)
+ }
+ }
+
+ // Commit changes
+ commitMsg := fmt.Sprintf("Create %d subtasks for task %s and mark parent as completed\n\nGenerated by Staff AI Agent System\n\nFiles modified:\n- %s.md (marked as completed)\n\nCreated individual task files:\n",
+ len(analysis.Subtasks), analysis.ParentTaskID, analysis.ParentTaskID)
+
+ // Add list of created files to commit message
+ for i := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ commitMsg += fmt.Sprintf("- %s.md\n", taskID)
+ }
+
+ if len(analysis.AgentCreations) > 0 {
+ commitMsg += fmt.Sprintf("\nProposed %d new agents for specialized skills", len(analysis.AgentCreations))
+ }
+ cmd = gitCmd("commit", "-m", commitMsg)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to commit: %w", err)
+ }
+
+ // Push branch
+ cmd = gitCmd("push", "-u", "origin", branchName)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to push branch: %w", err)
+ }
+
+ gtm.logger.Info("Created subtask proposal branch", slog.String("branch", branchName))
+ return nil
+}
+
+// updateParentTaskAsCompleted updates the parent task file to mark it as completed
+func (gtm *GitTaskManager) updateParentTaskAsCompleted(taskFilePath string, analysis *tm.SubtaskAnalysis) error {
+ // Read the existing parent task file
+ content, err := os.ReadFile(taskFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to read parent task file: %w", err)
+ }
+
+ taskContent := string(content)
+
+ // Find the YAML frontmatter boundaries
+ lines := strings.Split(taskContent, "\n")
+ var frontmatterStart, frontmatterEnd int = -1, -1
+
+ for i, line := range lines {
+ if line == "---" {
+ if frontmatterStart == -1 {
+ frontmatterStart = i
+ } else {
+ frontmatterEnd = i
+ break
+ }
+ }
+ }
+
+ if frontmatterStart == -1 || frontmatterEnd == -1 {
+ return fmt.Errorf("invalid task file format: missing YAML frontmatter")
+ }
+
+ // Update the frontmatter
+ now := time.Now().Format(time.RFC3339)
+ var updatedLines []string
+
+ // Add lines before frontmatter
+ updatedLines = append(updatedLines, lines[:frontmatterStart+1]...)
+
+ // Process frontmatter lines
+ for i := frontmatterStart + 1; i < frontmatterEnd; i++ {
+ line := lines[i]
+ if strings.HasPrefix(line, "status:") {
+ updatedLines = append(updatedLines, "status: completed")
+ } else if strings.HasPrefix(line, "updated_at:") {
+ updatedLines = append(updatedLines, fmt.Sprintf("updated_at: %s", now))
+ } else if strings.HasPrefix(line, "completed_at:") {
+ updatedLines = append(updatedLines, fmt.Sprintf("completed_at: %s", now))
+ } else {
+ updatedLines = append(updatedLines, line)
+ }
+ }
+
+ // Add closing frontmatter and rest of content
+ updatedLines = append(updatedLines, lines[frontmatterEnd:]...)
+
+ // Add subtask information to the task description
+ if frontmatterEnd+1 < len(lines) {
+ // Add subtask information
+ subtaskInfo := fmt.Sprintf("\n\n## Subtasks Created\n\nThis task has been broken down into %d subtasks:\n\n", len(analysis.Subtasks))
+ for i, subtask := range analysis.Subtasks {
+ subtaskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ subtaskInfo += fmt.Sprintf("- **%s**: %s (assigned to %s)\n", subtaskID, subtask.Title, subtask.AssignedTo)
+ }
+ subtaskInfo += fmt.Sprintf("\n**Total Estimated Hours:** %d\n", analysis.EstimatedTotalHours)
+ subtaskInfo += fmt.Sprintf("**Completed:** %s - Task broken down into actionable subtasks\n", now)
+
+ // Insert subtask info before any existing body content
+ updatedContent := strings.Join(updatedLines[:], "\n") + subtaskInfo
+
+ // Write the updated content back to the file
+ if err := os.WriteFile(taskFilePath, []byte(updatedContent), 0644); err != nil {
+ return fmt.Errorf("failed to write updated parent task file: %w", err)
+ }
+ }
+
+ gtm.logger.Info("Updated parent task to completed status", slog.String("task_id", analysis.ParentTaskID))
+ return nil
+}
+
+// generateSubtaskFile creates the content for an individual subtask file
+func (gtm *GitTaskManager) generateSubtaskFile(subtask tm.SubtaskProposal, taskID, parentTaskID string) string {
+ var content strings.Builder
+
+ // Generate YAML frontmatter
+ content.WriteString("---\n")
+ content.WriteString(fmt.Sprintf("id: %s\n", taskID))
+ content.WriteString(fmt.Sprintf("title: %s\n", subtask.Title))
+ content.WriteString(fmt.Sprintf("description: %s\n", subtask.Description))
+ content.WriteString(fmt.Sprintf("assignee: %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("owner_id: %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("owner_name: %s\n", subtask.AssignedTo))
+ content.WriteString("status: todo\n")
+ content.WriteString(fmt.Sprintf("priority: %s\n", strings.ToLower(string(subtask.Priority))))
+ content.WriteString(fmt.Sprintf("parent_task_id: %s\n", parentTaskID))
+ content.WriteString(fmt.Sprintf("estimated_hours: %d\n", subtask.EstimatedHours))
+ content.WriteString(fmt.Sprintf("created_at: %s\n", time.Now().Format(time.RFC3339)))
+ content.WriteString(fmt.Sprintf("updated_at: %s\n", time.Now().Format(time.RFC3339)))
+ content.WriteString("completed_at: null\n")
+ content.WriteString("archived_at: null\n")
+
+ // Add dependencies if any
+ if len(subtask.Dependencies) > 0 {
+ content.WriteString("dependencies:\n")
+ for _, dep := range subtask.Dependencies {
+ // Convert dependency index to actual subtask ID
+ if depIndex := parseDependencyIndex(dep); depIndex >= 0 {
+ depTaskID := fmt.Sprintf("%s-subtask-%d", parentTaskID, depIndex+1)
+ content.WriteString(fmt.Sprintf(" - %s\n", depTaskID))
+ }
+ }
+ }
+
+ // Add required skills if any
+ if len(subtask.RequiredSkills) > 0 {
+ content.WriteString("required_skills:\n")
+ for _, skill := range subtask.RequiredSkills {
+ content.WriteString(fmt.Sprintf(" - %s\n", skill))
+ }
+ }
+
+ content.WriteString("---\n\n")
+
+ // Add markdown content
+ content.WriteString("# Task Description\n\n")
+ content.WriteString(fmt.Sprintf("%s\n\n", subtask.Description))
+
+ if subtask.EstimatedHours > 0 {
+ content.WriteString("## Estimated Effort\n\n")
+ content.WriteString(fmt.Sprintf("**Estimated Hours:** %d\n\n", subtask.EstimatedHours))
+ }
+
+ if len(subtask.RequiredSkills) > 0 {
+ content.WriteString("## Required Skills\n\n")
+ for _, skill := range subtask.RequiredSkills {
+ content.WriteString(fmt.Sprintf("- %s\n", skill))
+ }
+ content.WriteString("\n")
+ }
+
+ if len(subtask.Dependencies) > 0 {
+ content.WriteString("## Dependencies\n\n")
+ content.WriteString("This task depends on the completion of:\n\n")
+ for _, dep := range subtask.Dependencies {
+ if depIndex := parseDependencyIndex(dep); depIndex >= 0 {
+ depTaskID := fmt.Sprintf("%s-subtask-%d", parentTaskID, depIndex+1)
+ content.WriteString(fmt.Sprintf("- %s\n", depTaskID))
+ }
+ }
+ content.WriteString("\n")
+ }
+
+ content.WriteString("## Notes\n\n")
+ content.WriteString(fmt.Sprintf("This subtask was generated from parent task: %s\n", parentTaskID))
+ content.WriteString("Generated by Staff AI Agent System\n\n")
+
+ return content.String()
+}
+
+func (gtm *GitTaskManager) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
+ var content strings.Builder
+
+ content.WriteString(fmt.Sprintf("# Subtasks Created for Task %s\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("This PR creates **%d individual task files** in `/operations/tasks/` ready for agent assignment.\n\n", len(analysis.Subtasks)))
+ content.WriteString(fmt.Sprintf("✅ **Parent task `%s` has been marked as completed** - the complex task has been successfully broken down into actionable subtasks.\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
+ content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
+ content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
+
+ // List the created task files
+ content.WriteString("## Created Task Files\n\n")
+ for i, subtask := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ content.WriteString(fmt.Sprintf("### %d. `%s.md`\n", i+1, taskID))
+ content.WriteString(fmt.Sprintf("- **Title:** %s\n", subtask.Title))
+ content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+ content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+ content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+ }
+
+ if analysis.RiskAssessment != "" {
+ content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
+ }
+
+ content.WriteString("## Proposed Subtasks\n\n")
+
+ for i, subtask := range analysis.Subtasks {
+ content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
+ content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+ content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+
+ if len(subtask.Dependencies) > 0 {
+ deps := strings.Join(subtask.Dependencies, ", ")
+ content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
+ }
+
+ content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+ }
+
+ content.WriteString("---\n")
+ content.WriteString("*Generated by Staff AI Agent System*\n\n")
+ content.WriteString("**Instructions:**\n")
+ content.WriteString("- Review the proposed subtasks\n")
+ content.WriteString("- Approve or request changes\n")
+ content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
+
+ return content.String()
+}
+
+func (gtm *GitTaskManager) determineBaseBranch(ctx context.Context) string {
+ // Get clone path to check branches
+ clonePath, err := gtm.git.GetAgentClonePath("subtask-service")
+ if err != nil {
+ gtm.logger.Warn("Failed to get clone path for base branch detection", slog.String("error", err.Error()))
+ return "main"
+ }
+
+ // Check if main branch exists
+ gitCmd := func(args ...string) *exec.Cmd {
+ return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
+ }
+
+ // Try to checkout main branch
+ cmd := gitCmd("show-ref", "refs/remotes/origin/main")
+ if err := cmd.Run(); err == nil {
+ return "main"
+ }
+
+ // Try to checkout master branch
+ cmd = gitCmd("show-ref", "refs/remotes/origin/master")
+ if err := cmd.Run(); err == nil {
+ return "master"
+ }
+
+ // Default to main if neither can be detected
+ gtm.logger.Warn("Could not determine base branch, defaulting to 'main'")
+ return "main"
+}
+
+// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
+func (gtm *GitTaskManager) createSolutionBranch(ctx context.Context, task *tm.Task, solution, branchName, agentName string) error {
+ // Get agent's dedicated Git clone
+ clonePath, err := gtm.git.GetAgentClonePath(agentName)
+ if err != nil {
+ return fmt.Errorf("failed to get agent clone: %w", err)
+ }
+
+ gtm.logger.Info("Agent working in clone",
+ slog.String("agent", agentName),
+ slog.String("clone_path", clonePath))
+
+ // Refresh the clone with latest changes
+ if err := gtm.git.RefreshAgentClone(agentName); err != nil {
+ gtm.logger.Warn("Failed to refresh clone for agent",
+ slog.String("agent", agentName),
+ slog.String("error", err.Error()))
+ }
+
+ // All Git operations use the agent's clone directory
+ gitCmd := func(args ...string) *exec.Cmd {
+ return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
+ }
+
+ // Ensure we're on main branch before creating new branch
+ cmd := gitCmd("checkout", "main")
+ if err := cmd.Run(); err != nil {
+ // Try master branch if main doesn't exist
+ cmd = gitCmd("checkout", "master")
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to checkout main/master branch: %w", err)
+ }
+ }
+
+ // Create branch
+ cmd = gitCmd("checkout", "-b", branchName)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to create branch: %w", err)
+ }
+
+ // Create solution file in agent's clone
+ solutionDir := filepath.Join(clonePath, "tasks", "solutions")
+ if err := os.MkdirAll(solutionDir, 0755); err != nil {
+ return fmt.Errorf("failed to create solution directory: %w", err)
+ }
+
+ solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
+ solutionContent := fmt.Sprintf(`# Solution for Task: %s
+
+**Agent:** %s
+**Completed:** %s
+
+## Task Description
+%s
+
+## Solution
+%s
+
+---
+*Generated by Staff AI Agent System*
+`, task.Title, agentName, time.Now().Format(time.RFC3339), task.Description, solution)
+
+ if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
+ return fmt.Errorf("failed to write solution file: %w", err)
+ }
+
+ // Stage files
+ relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
+ cmd = gitCmd("add", relativeSolutionFile)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to stage files: %w", err)
+ }
+
+ // Commit changes
+ commitMsg := buildCommitMessage(task, gtm.config.Git.CommitMessageTemplate, agentName)
+ cmd = gitCmd("commit", "-m", commitMsg)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to commit: %w", err)
+ }
+
+ // Push branch
+ cmd = gitCmd("push", "-u", "origin", branchName)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to push branch: %w", err)
+ }
+
+ gtm.logger.Info("Agent successfully pushed branch",
+ slog.String("agent", agentName),
+ slog.String("branch", branchName))
+ return nil
+}
+
+func buildCommitMessage(task *tm.Task, template, agentName string) string {
+ replacements := map[string]string{
+ "{task_id}": task.ID,
+ "{task_title}": task.Title,
+ "{agent_name}": agentName,
+ "{solution}": "See solution file for details",
+ }
+
+ result := template
+ for placeholder, value := range replacements {
+ result = strings.ReplaceAll(result, placeholder, value)
+ }
+
+ return result
+}
+
+// parseDependencyIndex parses a dependency string to an integer index
+func parseDependencyIndex(dep string) int {
+ var idx int
+ if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
+ return idx
+ }
+ return -1 // Invalid dependency format
+}
+
+// generateBranchName creates a Git branch name for the task
+func generateBranchName(prefix string, task *tm.Task) string {
+ // Clean title for use in branch name
+ cleanTitle := strings.ToLower(task.Title)
+ cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
+ cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
+ // Remove special characters
+ var result strings.Builder
+ for _, r := range cleanTitle {
+ if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
+ result.WriteRune(r)
+ }
+ }
+ cleanTitle = result.String()
+
+ // Limit length
+ if len(cleanTitle) > 40 {
+ cleanTitle = cleanTitle[:40]
+ }
+
+ return fmt.Sprintf("%s%s-%s", prefix, task.ID, cleanTitle)
+}
+
+// buildSolutionPRDescription creates PR description from template
+func buildSolutionPRDescription(task *tm.Task, solution, template, agentName string) string {
+ // Truncate solution for PR if too long
+ truncatedSolution := solution
+ if len(solution) > 1000 {
+ truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
+ }
+
+ replacements := map[string]string{
+ "{task_id}": task.ID,
+ "{task_title}": task.Title,
+ "{task_description}": task.Description,
+ "{agent_name}": fmt.Sprintf("%s", agentName),
+ "{priority}": string(task.Priority),
+ "{solution}": truncatedSolution,
+ "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
+ }
+
+ result := template
+ for placeholder, value := range replacements {
+ result = strings.ReplaceAll(result, placeholder, value)
+ }
+
+ return result
+}
+
// Ensure GitTaskManager implements TaskManager interface
var _ tm.TaskManager = (*GitTaskManager)(nil)