blob: c1a1bc99abce00b1ec6c7fa627aed048edb89961 [file] [log] [blame]
package subtasks
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/iomodo/staff/git"
"github.com/iomodo/staff/llm"
"github.com/iomodo/staff/tm"
)
// SubtaskService handles subtask generation and management
type SubtaskService struct {
llmProvider llm.LLMProvider
taskManager tm.TaskManager
agentRoles []string // Available agent roles for assignment
prProvider git.PullRequestProvider // GitHub PR provider
githubOwner string
githubRepo string
cloneManager *git.CloneManager
}
// NewSubtaskService creates a new subtask service
func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string, prProvider git.PullRequestProvider, githubOwner, githubRepo string, cloneManager *git.CloneManager) *SubtaskService {
return &SubtaskService{
llmProvider: provider,
taskManager: taskManager,
agentRoles: agentRoles,
prProvider: prProvider,
githubOwner: githubOwner,
githubRepo: githubRepo,
cloneManager: cloneManager,
}
}
// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
func (s *SubtaskService) ShouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
prompt := s.buildSubtaskDecisionPrompt(task)
req := llm.ChatCompletionRequest{
Model: "gpt-4",
Messages: []llm.Message{
{
Role: llm.RoleSystem,
Content: s.getSubtaskDecisionSystemPrompt(),
},
{
Role: llm.RoleUser,
Content: prompt,
},
},
MaxTokens: &[]int{1000}[0],
Temperature: &[]float64{0.3}[0],
}
resp, err := s.llmProvider.ChatCompletion(ctx, req)
if err != nil {
return nil, fmt.Errorf("LLM decision failed: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("no response from LLM")
}
// Parse the LLM response
decision, err := s.parseSubtaskDecision(resp.Choices[0].Message.Content)
if err != nil {
return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
}
return decision, nil
}
// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
prompt := s.buildSubtaskAnalysisPrompt(task)
req := llm.ChatCompletionRequest{
Model: "gpt-4",
Messages: []llm.Message{
{
Role: llm.RoleSystem,
Content: s.getSubtaskAnalysisSystemPrompt(),
},
{
Role: llm.RoleUser,
Content: prompt,
},
},
MaxTokens: &[]int{4000}[0],
Temperature: &[]float64{0.3}[0],
}
resp, err := s.llmProvider.ChatCompletion(ctx, req)
if err != nil {
return nil, fmt.Errorf("LLM analysis failed: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("no response from LLM")
}
// Parse the LLM response
analysis, err := s.parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse LLM response: %w", err)
}
return analysis, nil
}
// getSubtaskDecisionSystemPrompt returns the system prompt for subtask decision
func (s *SubtaskService) getSubtaskDecisionSystemPrompt() string {
availableRoles := strings.Join(s.agentRoles, ", ")
return fmt.Sprintf(`You are an expert project manager and task analyst. Your job is to determine whether a task needs to be broken down into subtasks.
Currently available team roles and their capabilities: %s
When evaluating a task, consider:
1. Task complexity and scope
2. Whether multiple specialized skills are needed
3. If the task can be completed by a single agent with current capabilities
4. Whether new agent roles might be needed for specialized skills
Respond with a JSON object in this exact format:
{
"needs_subtasks": true/false,
"reasoning": "Clear explanation of why subtasks are or aren't needed",
"complexity_score": 5,
"required_skills": ["skill1", "skill2", "skill3"]
}
Complexity score should be 1-10 where:
- 1-3: Simple tasks that can be handled by one agent
- 4-6: Moderate complexity, might benefit from subtasks
- 7-10: Complex tasks that definitely need breaking down
Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
}
// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
availableRoles := strings.Join(s.agentRoles, ", ")
return fmt.Sprintf(`You are an expert project manager and technical architect. Your job is to analyze complex tasks and break them down into well-defined subtasks that can be assigned to specialized team members.
Currently available team roles: %s
When analyzing a task, you should:
1. Understand the task requirements and scope
2. Break it down into logical, manageable subtasks
3. Assign each subtask to the most appropriate team role OR propose creating new agents
4. Estimate effort and identify dependencies
5. Provide a clear execution strategy
If you need specialized skills not covered by existing roles, propose new agent creation.
Respond with a JSON object in this exact format:
{
"analysis_summary": "Brief analysis of the task and approach",
"subtasks": [
{
"title": "Subtask title",
"description": "Detailed description of what needs to be done",
"priority": "high|medium|low",
"assigned_to": "role_name",
"estimated_hours": 8,
"dependencies": ["subtask_index_1", "subtask_index_2"],
"required_skills": ["skill1", "skill2"]
}
],
"agent_creations": [
{
"role": "new_role_name",
"skills": ["specialized_skill1", "specialized_skill2"],
"description": "Description of what this agent does",
"justification": "Why this new agent is needed"
}
],
"recommended_approach": "High-level strategy for executing these subtasks",
"estimated_total_hours": 40,
"risk_assessment": "Potential risks and mitigation strategies"
}
For existing roles, use: %s
For new agents, propose appropriate role names and skill sets.
Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
}
// buildSubtaskDecisionPrompt creates the user prompt for subtask decision
func (s *SubtaskService) buildSubtaskDecisionPrompt(task *tm.Task) string {
return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
**Task Title:** %s
**Description:** %s
**Priority:** %s
**Current Status:** %s
Consider:
- Can this be completed by a single agent with existing capabilities?
- Does it require multiple specialized skills?
- Is the scope too large for one person?
- Are there logical components that could be parallelized?
Provide your decision in the JSON format specified in the system prompt.`,
task.Title,
task.Description,
task.Priority,
task.Status)
}
// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
func (s *SubtaskService) buildSubtaskAnalysisPrompt(task *tm.Task) string {
return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
**Task Title:** %s
**Description:** %s
**Priority:** %s
**Current Status:** %s
Please analyze this task and provide a detailed breakdown into subtasks. Consider:
- Technical complexity and requirements
- Logical task dependencies
- Appropriate skill sets needed for each subtask
- Risk factors and potential blockers
- Estimated effort for each component
Provide the analysis in the JSON format specified in the system prompt.`,
task.Title,
task.Description,
task.Priority,
task.Status)
}
// parseSubtaskDecision parses the LLM response into a SubtaskDecision struct
func (s *SubtaskService) parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
// Try to extract JSON from the response
jsonStart := strings.Index(response, "{")
jsonEnd := strings.LastIndex(response, "}")
if jsonStart == -1 || jsonEnd == -1 {
return nil, fmt.Errorf("no JSON found in LLM response")
}
jsonStr := response[jsonStart : jsonEnd+1]
var decision tm.SubtaskDecision
if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
return &decision, nil
}
// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
// Try to extract JSON from the response (LLM might wrap it in markdown)
jsonStart := strings.Index(response, "{")
jsonEnd := strings.LastIndex(response, "}")
if jsonStart == -1 || jsonEnd == -1 {
return nil, fmt.Errorf("no JSON found in LLM response")
}
jsonStr := response[jsonStart : jsonEnd+1]
var rawAnalysis struct {
AnalysisSummary string `json:"analysis_summary"`
Subtasks []struct {
Title string `json:"title"`
Description string `json:"description"`
Priority string `json:"priority"`
AssignedTo string `json:"assigned_to"`
EstimatedHours int `json:"estimated_hours"`
Dependencies []string `json:"dependencies"`
RequiredSkills []string `json:"required_skills"`
} `json:"subtasks"`
AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
RecommendedApproach string `json:"recommended_approach"`
EstimatedTotalHours int `json:"estimated_total_hours"`
RiskAssessment string `json:"risk_assessment"`
}
if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
// Convert to our types
analysis := &tm.SubtaskAnalysis{
ParentTaskID: parentTaskID,
AnalysisSummary: rawAnalysis.AnalysisSummary,
AgentCreations: rawAnalysis.AgentCreations,
RecommendedApproach: rawAnalysis.RecommendedApproach,
EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
RiskAssessment: rawAnalysis.RiskAssessment,
}
// Convert subtasks
for _, st := range rawAnalysis.Subtasks {
priority := tm.PriorityMedium // default
switch strings.ToLower(st.Priority) {
case "high":
priority = tm.PriorityHigh
case "low":
priority = tm.PriorityLow
}
subtask := tm.SubtaskProposal{
Title: st.Title,
Description: st.Description,
Priority: priority,
AssignedTo: st.AssignedTo,
EstimatedHours: st.EstimatedHours,
Dependencies: st.Dependencies,
RequiredSkills: st.RequiredSkills,
}
analysis.Subtasks = append(analysis.Subtasks, subtask)
}
// Validate agent assignments and handle new agent creation
if err := s.validateAndHandleAgentAssignments(analysis); err != nil {
log.Printf("Warning during agent assignment handling: %v", err)
}
return analysis, nil
}
// validateAndHandleAgentAssignments validates assignments and creates agent creation subtasks if needed
func (s *SubtaskService) validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis) error {
// Collect all agent roles that will be available (existing + proposed new ones)
availableRoles := make(map[string]bool)
for _, role := range s.agentRoles {
availableRoles[role] = true
}
// Add proposed new agent roles
for _, agentCreation := range analysis.AgentCreations {
availableRoles[agentCreation.Role] = true
// Create a subtask for agent creation
agentCreationSubtask := tm.SubtaskProposal{
Title: fmt.Sprintf("Create %s Agent", strings.Title(agentCreation.Role)),
Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
Priority: tm.PriorityHigh, // Agent creation is high priority
AssignedTo: "ceo", // CEO creates new agents
EstimatedHours: 4, // Estimated time to set up new agent
Dependencies: []string{}, // No dependencies for agent creation
RequiredSkills: []string{"agent_configuration", "system_design"},
}
// Insert at the beginning so agent creation happens first
analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
// Update dependencies to account for the new subtask at index 0
for i := 1; i < len(analysis.Subtasks); i++ {
for j, dep := range analysis.Subtasks[i].Dependencies {
// Convert dependency index and increment by 1
if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
}
}
}
}
// Now validate all assignments against available roles
defaultRole := "ceo" // fallback role
if len(s.agentRoles) > 0 {
defaultRole = s.agentRoles[0]
}
for i := range analysis.Subtasks {
if !availableRoles[analysis.Subtasks[i].AssignedTo] {
log.Printf("Warning: Unknown agent role '%s' for subtask '%s', assigning to %s",
analysis.Subtasks[i].AssignedTo, analysis.Subtasks[i].Title, defaultRole)
analysis.Subtasks[i].AssignedTo = defaultRole
}
}
return nil
}
// parseDependencyIndex parses a dependency string to an integer index
func (s *SubtaskService) parseDependencyIndex(dep string) int {
var idx int
if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
return idx
}
return -1 // Invalid dependency format
}
// isValidAgentRole checks if a role is in the available agent roles
func (s *SubtaskService) isValidAgentRole(role string) bool {
for _, availableRole := range s.agentRoles {
if availableRole == role {
return true
}
}
return false
}
// GenerateSubtaskPR creates a PR with the proposed subtasks
func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
if s.prProvider == nil {
return "", fmt.Errorf("PR provider not configured")
}
// Generate branch name for subtask proposal
branchName := fmt.Sprintf("subtasks/%s-proposal", analysis.ParentTaskID)
log.Printf("Creating subtask PR with branch: %s", branchName)
// Create Git branch and commit subtask proposal
if err := s.createSubtaskBranch(ctx, analysis, branchName); err != nil {
return "", fmt.Errorf("failed to create subtask branch: %w", err)
}
// Generate PR content
prContent := s.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 := s.determineBaseBranch(ctx)
log.Printf("Using base branch: %s", baseBranch)
// Create the pull request
options := git.PullRequestOptions{
Title: title,
Description: prContent,
HeadBranch: branchName,
BaseBranch: baseBranch,
Labels: []string{"subtasks", "proposal", "ai-generated"},
Draft: false,
}
log.Printf("Creating PR with options: title=%s, head=%s, base=%s", options.Title, options.HeadBranch, options.BaseBranch)
pr, err := s.prProvider.CreatePullRequest(ctx, options)
if err != nil {
return "", fmt.Errorf("failed to create PR: %w", err)
}
prURL := fmt.Sprintf("https://github.com/%s/%s/pull/%d", s.githubOwner, s.githubRepo, pr.Number)
log.Printf("Generated subtask proposal PR: %s", prURL)
return prURL, nil
}
// determineBaseBranch determines the correct base branch (main or master)
func (s *SubtaskService) determineBaseBranch(ctx context.Context) string {
if s.cloneManager == nil {
return "main" // default fallback
}
// Get clone path to check branches
clonePath, err := s.cloneManager.GetAgentClonePath("subtask-service")
if err != nil {
log.Printf("Warning: failed to get clone path for base branch detection: %v", err)
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
log.Printf("Warning: Could not determine base branch, defaulting to 'main'")
return "main"
}
// generateSubtaskFile creates the content for an individual subtask file
func (s *SubtaskService) 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(fmt.Sprintf("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 := s.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 := s.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()
}
// updateParentTaskAsCompleted updates the parent task file to mark it as completed
func (s *SubtaskService) 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[:len(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)
}
}
log.Printf("Updated parent task %s to completed status", analysis.ParentTaskID)
return nil
}
// generateSubtaskPRContent creates markdown content for the subtask proposal PR
func (s *SubtaskService) 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()
}
// createSubtaskBranch creates a Git branch with subtask proposal content
func (s *SubtaskService) createSubtaskBranch(ctx context.Context, analysis *tm.SubtaskAnalysis, branchName string) error {
if s.cloneManager == nil {
return fmt.Errorf("clone manager not configured")
}
// Get a temporary clone for creating the subtask branch
clonePath, err := s.cloneManager.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 {
log.Printf("Warning: failed to pull latest changes: %v", err)
}
// 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 := s.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)
log.Printf("Updated parent task file: %s", 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 := s.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)
log.Printf("Created subtask file: %s", 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)
}
log.Printf("Created subtask proposal branch: %s", branchName)
return nil
}
// Close cleans up the service
func (s *SubtaskService) Close() error {
if s.llmProvider != nil {
return s.llmProvider.Close()
}
return nil
}