Refactor subtasks

Change-Id: I5ea6ffe266b8d6010de46bbf3bc6d7f861600f00
diff --git a/server/task/auto_assignment.go b/server/task/auto_assignment.go
new file mode 100644
index 0000000..4bd5632
--- /dev/null
+++ b/server/task/auto_assignment.go
@@ -0,0 +1,237 @@
+package task
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/iomodo/staff/config"
+	"github.com/iomodo/staff/tm"
+)
+
+// AutoAssigner handles intelligent task assignment to agents
+type AutoAssigner struct {
+	agents []config.AgentConfig
+}
+
+// NewAutoAssigner creates a new auto-assignment system
+func NewAutoAssigner(agents []config.AgentConfig) *AutoAssigner {
+	return &AutoAssigner{
+		agents: agents,
+	}
+}
+
+// AssignmentScore represents how well an agent matches a task
+type AssignmentScore struct {
+	AgentName string
+	Score     float64
+	Reasons   []string
+}
+
+// AssignTask automatically assigns a task to the best matching agent
+func (a *AutoAssigner) AssignTask(task *tm.Task) (string, error) {
+	if len(a.agents) == 0 {
+		return "", fmt.Errorf("no agents available for assignment")
+	}
+
+	scores := a.calculateScores(task)
+	if len(scores) == 0 {
+		return "", fmt.Errorf("no suitable agent found for task")
+	}
+
+	// Sort by score (highest first)
+	sort.Slice(scores, func(i, j int) bool {
+		return scores[i].Score > scores[j].Score
+	})
+
+	bestMatch := scores[0]
+	if bestMatch.Score == 0 {
+		// No good match, assign to CEO as fallback
+		return "ceo", nil
+	}
+
+	return bestMatch.AgentName, nil
+}
+
+// GetAssignmentRecommendations returns ranked recommendations for task assignment
+func (a *AutoAssigner) GetAssignmentRecommendations(task *tm.Task) []AssignmentScore {
+	scores := a.calculateScores(task)
+
+	// Sort by score (highest first)
+	sort.Slice(scores, func(i, j int) bool {
+		return scores[i].Score > scores[j].Score
+	})
+
+	return scores
+}
+
+// calculateScores calculates assignment scores for all agents
+func (a *AutoAssigner) calculateScores(task *tm.Task) []AssignmentScore {
+	scores := make([]AssignmentScore, 0, len(a.agents))
+
+	taskText := strings.ToLower(task.Title + " " + task.Description)
+	taskKeywords := extractKeywords(taskText)
+
+	for _, agent := range a.agents {
+		score := &AssignmentScore{
+			AgentName: agent.Name,
+			Score:     0,
+			Reasons:   make([]string, 0),
+		}
+
+		// Score based on task type keywords
+		score.Score += a.scoreTaskTypes(agent.TaskTypes, taskKeywords, score)
+
+		// Score based on capabilities
+		score.Score += a.scoreCapabilities(agent.Capabilities, taskKeywords, score)
+
+		// Score based on task priority and agent model
+		score.Score += a.scorePriorityModelMatch(task.Priority, agent.Model, score)
+
+		// Score based on explicit agent mention in task
+		score.Score += a.scoreExplicitMention(agent.Name, agent.Role, taskText, score)
+
+		scores = append(scores, *score)
+	}
+
+	return scores
+}
+
+// scoreTaskTypes scores based on task type matching
+func (a *AutoAssigner) scoreTaskTypes(agentTypes []string, taskKeywords []string, score *AssignmentScore) float64 {
+	typeScore := 0.0
+
+	for _, agentType := range agentTypes {
+		for _, keyword := range taskKeywords {
+			if strings.Contains(keyword, agentType) || strings.Contains(agentType, keyword) {
+				typeScore += 3.0
+				score.Reasons = append(score.Reasons, fmt.Sprintf("matches task type: %s", agentType))
+			}
+		}
+	}
+
+	return typeScore
+}
+
+// scoreCapabilities scores based on capability matching
+func (a *AutoAssigner) scoreCapabilities(capabilities []string, taskKeywords []string, score *AssignmentScore) float64 {
+	capScore := 0.0
+
+	for _, capability := range capabilities {
+		for _, keyword := range taskKeywords {
+			if strings.Contains(keyword, capability) || strings.Contains(capability, keyword) {
+				capScore += 2.0
+				score.Reasons = append(score.Reasons, fmt.Sprintf("has capability: %s", capability))
+			}
+		}
+	}
+
+	return capScore
+}
+
+// scorePriorityModelMatch scores based on priority and model sophistication
+func (a *AutoAssigner) scorePriorityModelMatch(priority tm.TaskPriority, model string, score *AssignmentScore) float64 {
+	priorityScore := 0.0
+
+	// High priority tasks prefer more capable models
+	if priority == tm.PriorityHigh && strings.Contains(model, "gpt-4") {
+		priorityScore += 1.0
+		score.Reasons = append(score.Reasons, "high priority task matches advanced model")
+	}
+
+	// Medium/low priority can use efficient models
+	if priority != tm.PriorityHigh && strings.Contains(model, "gpt-3.5") {
+		priorityScore += 0.5
+		score.Reasons = append(score.Reasons, "priority matches model efficiency")
+	}
+
+	return priorityScore
+}
+
+// scoreExplicitMention scores based on explicit agent/role mentions
+func (a *AutoAssigner) scoreExplicitMention(agentName, agentRole, taskText string, score *AssignmentScore) float64 {
+	mentionScore := 0.0
+
+	// Check for explicit agent name mention
+	if strings.Contains(taskText, agentName) {
+		mentionScore += 5.0
+		score.Reasons = append(score.Reasons, "explicitly mentioned by name")
+	}
+
+	// Check for role mention
+	roleLower := strings.ToLower(agentRole)
+	if strings.Contains(taskText, roleLower) {
+		mentionScore += 4.0
+		score.Reasons = append(score.Reasons, "role mentioned in task")
+	}
+
+	return mentionScore
+}
+
+// extractKeywords extracts relevant keywords from task text
+func extractKeywords(text string) []string {
+	// Simple keyword extraction - split by common delimiters
+	words := strings.FieldsFunc(text, func(c rune) bool {
+		return c == ' ' || c == ',' || c == '.' || c == ':' || c == ';' || c == '\n' || c == '\t'
+	})
+
+	// Filter out common stop words and short words
+	stopWords := map[string]bool{
+		"the": true, "a": true, "an": true, "and": true, "or": true, "but": true,
+		"in": true, "on": true, "at": true, "to": true, "for": true, "of": true,
+		"with": true, "by": true, "is": true, "are": true, "was": true, "were": true,
+		"be": true, "been": true, "have": true, "has": true, "had": true, "do": true,
+		"does": true, "did": true, "will": true, "would": true, "could": true, "should": true,
+	}
+
+	keywords := make([]string, 0)
+	for _, word := range words {
+		word = strings.ToLower(strings.TrimSpace(word))
+		if len(word) > 2 && !stopWords[word] {
+			keywords = append(keywords, word)
+		}
+	}
+
+	return keywords
+}
+
+// ValidateAssignment checks if an assignment is valid
+func (a *AutoAssigner) ValidateAssignment(agentName string, task *tm.Task) error {
+	// Check if agent exists
+	for _, agent := range a.agents {
+		if agent.Name == agentName {
+			return nil
+		}
+	}
+
+	return fmt.Errorf("agent '%s' not found", agentName)
+}
+
+// GetAgentCapabilities returns the capabilities of a specific agent
+func (a *AutoAssigner) GetAgentCapabilities(agentName string) ([]string, error) {
+	for _, agent := range a.agents {
+		if agent.Name == agentName {
+			return agent.Capabilities, nil
+		}
+	}
+
+	return nil, fmt.Errorf("agent '%s' not found", agentName)
+}
+
+// GetRecommendationExplanation returns a human-readable explanation for assignment
+func (a *AutoAssigner) GetRecommendationExplanation(task *tm.Task, agentName string) string {
+	recommendations := a.GetAssignmentRecommendations(task)
+
+	for _, rec := range recommendations {
+		if rec.AgentName == agentName {
+			if len(rec.Reasons) == 0 {
+				return fmt.Sprintf("Agent %s assigned (score: %.1f)", agentName, rec.Score)
+			}
+
+			reasons := strings.Join(rec.Reasons, ", ")
+			return fmt.Sprintf("Agent %s assigned (score: %.1f) because: %s", agentName, rec.Score, reasons)
+		}
+	}
+
+	return fmt.Sprintf("Agent %s assigned (manual override)", agentName)
+}