Fix warnings
Change-Id: I0b157e9d5a007e8a0fbfdb23af4635ea96bd11c6
diff --git a/server/go.mod b/server/go.mod
index d2079f1..7ab34ee 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -15,4 +15,5 @@
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
+ golang.org/x/text v0.27.0 // indirect
)
diff --git a/server/go.sum b/server/go.sum
index 12bafed..bface14 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -16,6 +16,8 @@
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/server/subtasks/service.go b/server/subtasks/service.go
index d704a33..d921006 100644
--- a/server/subtasks/service.go
+++ b/server/subtasks/service.go
@@ -14,18 +14,20 @@
"github.com/iomodo/staff/git"
"github.com/iomodo/staff/llm"
"github.com/iomodo/staff/tm"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
// 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
- logger *slog.Logger
+ 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
+ logger *slog.Logger
}
// NewSubtaskService creates a new subtask service
@@ -48,7 +50,7 @@
// 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{
@@ -86,7 +88,7 @@
// 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{
@@ -124,7 +126,7 @@
// 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
@@ -154,7 +156,7 @@
// 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
@@ -218,10 +220,10 @@
- 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,
+Provide your decision in the JSON format specified in the system prompt.`,
+ task.Title,
+ task.Description,
+ task.Priority,
task.Status)
}
@@ -244,10 +246,10 @@
- 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,
+Provide the analysis in the JSON format specified in the system prompt.`,
+ task.Title,
+ task.Description,
+ task.Priority,
task.Status)
}
@@ -256,18 +258,18 @@
// 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
}
@@ -276,16 +278,16 @@
// 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 {
+ AnalysisSummary string `json:"analysis_summary"`
+ Subtasks []struct {
Title string `json:"title"`
Description string `json:"description"`
Priority string `json:"priority"`
@@ -295,15 +297,15 @@
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"`
+ 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,
@@ -313,7 +315,7 @@
EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
RiskAssessment: rawAnalysis.RiskAssessment,
}
-
+
// Convert subtasks
for _, st := range rawAnalysis.Subtasks {
priority := tm.PriorityMedium // default
@@ -323,7 +325,7 @@
case "low":
priority = tm.PriorityLow
}
-
+
subtask := tm.SubtaskProposal{
Title: st.Title,
Description: st.Description,
@@ -333,15 +335,15 @@
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 {
s.logger.Warn("Warning during agent assignment handling", slog.String("error", err.Error()))
}
-
+
return analysis, nil
}
@@ -352,25 +354,25 @@
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)),
+ Title: fmt.Sprintf("Create %s Agent", cases.Title(language.English).String(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
+ 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 {
@@ -381,23 +383,23 @@
}
}
}
-
+
// 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] {
- s.logger.Warn("Unknown agent role for subtask, using default",
+ s.logger.Warn("Unknown agent role for subtask, using default",
slog.String("unknown_role", analysis.Subtasks[i].AssignedTo),
slog.String("subtask_title", analysis.Subtasks[i].Title),
slog.String("assigned_role", defaultRole))
analysis.Subtasks[i].AssignedTo = defaultRole
}
}
-
+
return nil
}
@@ -438,7 +440,7 @@
// 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")
@@ -515,7 +517,7 @@
// 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))
@@ -524,7 +526,7 @@
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("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))
@@ -532,7 +534,7 @@
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")
@@ -544,7 +546,7 @@
}
}
}
-
+
// Add required skills if any
if len(subtask.RequiredSkills) > 0 {
content.WriteString("required_skills:\n")
@@ -552,18 +554,18 @@
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 {
@@ -571,7 +573,7 @@
}
content.WriteString("\n")
}
-
+
if len(subtask.Dependencies) > 0 {
content.WriteString("## Dependencies\n\n")
content.WriteString("This task depends on the completion of:\n\n")
@@ -583,11 +585,11 @@
}
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()
}
@@ -600,11 +602,11 @@
}
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 {
@@ -615,18 +617,18 @@
}
}
}
-
+
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]
@@ -640,10 +642,10 @@
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
@@ -654,16 +656,16 @@
}
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
-
+ 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)
}
}
-
+
s.logger.Info("Updated parent task to completed status", slog.String("task_id", analysis.ParentTaskID))
return nil
}
@@ -671,14 +673,14 @@
// 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 {
@@ -690,34 +692,34 @@
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()
}
@@ -781,7 +783,7 @@
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)
@@ -812,15 +814,15 @@
}
// 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",
+ 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))
}
@@ -839,11 +841,10 @@
return nil
}
-
// Close cleans up the service
func (s *SubtaskService) Close() error {
if s.llmProvider != nil {
return s.llmProvider.Close()
}
return nil
-}
\ No newline at end of file
+}