blob: 5f99f740c8b31a61af596892dffbb3cff0a2c772 [file] [log] [blame]
iomodod9ff8da2025-07-28 11:42:22 +04001package subtasks
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
iomodo443b20a2025-07-28 15:24:05 +04008 "os"
9 "os/exec"
10 "path/filepath"
iomodod9ff8da2025-07-28 11:42:22 +040011 "strings"
iomodo443b20a2025-07-28 15:24:05 +040012 "time"
iomodod9ff8da2025-07-28 11:42:22 +040013
iomodo443b20a2025-07-28 15:24:05 +040014 "github.com/iomodo/staff/git"
iomodod9ff8da2025-07-28 11:42:22 +040015 "github.com/iomodo/staff/llm"
16 "github.com/iomodo/staff/tm"
17)
18
19// SubtaskService handles subtask generation and management
20type SubtaskService struct {
21 llmProvider llm.LLMProvider
22 taskManager tm.TaskManager
23 agentRoles []string // Available agent roles for assignment
iomodo443b20a2025-07-28 15:24:05 +040024 prProvider git.PullRequestProvider // GitHub PR provider
25 githubOwner string
26 githubRepo string
27 cloneManager *git.CloneManager
iomodod9ff8da2025-07-28 11:42:22 +040028}
29
30// NewSubtaskService creates a new subtask service
iomodo443b20a2025-07-28 15:24:05 +040031func NewSubtaskService(provider llm.LLMProvider, taskManager tm.TaskManager, agentRoles []string, prProvider git.PullRequestProvider, githubOwner, githubRepo string, cloneManager *git.CloneManager) *SubtaskService {
iomodod9ff8da2025-07-28 11:42:22 +040032 return &SubtaskService{
iomodo443b20a2025-07-28 15:24:05 +040033 llmProvider: provider,
34 taskManager: taskManager,
35 agentRoles: agentRoles,
36 prProvider: prProvider,
37 githubOwner: githubOwner,
38 githubRepo: githubRepo,
39 cloneManager: cloneManager,
iomodod9ff8da2025-07-28 11:42:22 +040040 }
41}
42
iomodo5c99a442025-07-28 14:23:52 +040043// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
44func (s *SubtaskService) ShouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
45 prompt := s.buildSubtaskDecisionPrompt(task)
46
47 req := llm.ChatCompletionRequest{
48 Model: "gpt-4",
49 Messages: []llm.Message{
50 {
51 Role: llm.RoleSystem,
52 Content: s.getSubtaskDecisionSystemPrompt(),
53 },
54 {
55 Role: llm.RoleUser,
56 Content: prompt,
57 },
58 },
59 MaxTokens: &[]int{1000}[0],
60 Temperature: &[]float64{0.3}[0],
61 }
62
63 resp, err := s.llmProvider.ChatCompletion(ctx, req)
64 if err != nil {
65 return nil, fmt.Errorf("LLM decision failed: %w", err)
66 }
67
68 if len(resp.Choices) == 0 {
69 return nil, fmt.Errorf("no response from LLM")
70 }
71
72 // Parse the LLM response
73 decision, err := s.parseSubtaskDecision(resp.Choices[0].Message.Content)
74 if err != nil {
75 return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
76 }
77
78 return decision, nil
79}
80
iomodod9ff8da2025-07-28 11:42:22 +040081// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
82func (s *SubtaskService) AnalyzeTaskForSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
83 prompt := s.buildSubtaskAnalysisPrompt(task)
84
85 req := llm.ChatCompletionRequest{
86 Model: "gpt-4",
87 Messages: []llm.Message{
88 {
89 Role: llm.RoleSystem,
90 Content: s.getSubtaskAnalysisSystemPrompt(),
91 },
92 {
93 Role: llm.RoleUser,
94 Content: prompt,
95 },
96 },
97 MaxTokens: &[]int{4000}[0],
98 Temperature: &[]float64{0.3}[0],
99 }
100
101 resp, err := s.llmProvider.ChatCompletion(ctx, req)
102 if err != nil {
103 return nil, fmt.Errorf("LLM analysis failed: %w", err)
104 }
105
106 if len(resp.Choices) == 0 {
107 return nil, fmt.Errorf("no response from LLM")
108 }
109
110 // Parse the LLM response
111 analysis, err := s.parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID)
112 if err != nil {
113 return nil, fmt.Errorf("failed to parse LLM response: %w", err)
114 }
115
116 return analysis, nil
117}
118
iomodo5c99a442025-07-28 14:23:52 +0400119// getSubtaskDecisionSystemPrompt returns the system prompt for subtask decision
120func (s *SubtaskService) getSubtaskDecisionSystemPrompt() string {
121 availableRoles := strings.Join(s.agentRoles, ", ")
122
123 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.
124
125Currently available team roles and their capabilities: %s
126
127When evaluating a task, consider:
1281. Task complexity and scope
1292. Whether multiple specialized skills are needed
1303. If the task can be completed by a single agent with current capabilities
1314. Whether new agent roles might be needed for specialized skills
132
133Respond with a JSON object in this exact format:
134{
135 "needs_subtasks": true/false,
136 "reasoning": "Clear explanation of why subtasks are or aren't needed",
137 "complexity_score": 5,
138 "required_skills": ["skill1", "skill2", "skill3"]
139}
140
141Complexity score should be 1-10 where:
142- 1-3: Simple tasks that can be handled by one agent
143- 4-6: Moderate complexity, might benefit from subtasks
144- 7-10: Complex tasks that definitely need breaking down
145
146Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
147}
148
iomodod9ff8da2025-07-28 11:42:22 +0400149// getSubtaskAnalysisSystemPrompt returns the system prompt for subtask analysis
150func (s *SubtaskService) getSubtaskAnalysisSystemPrompt() string {
151 availableRoles := strings.Join(s.agentRoles, ", ")
152
153 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.
154
iomodo5c99a442025-07-28 14:23:52 +0400155Currently available team roles: %s
iomodod9ff8da2025-07-28 11:42:22 +0400156
157When analyzing a task, you should:
1581. Understand the task requirements and scope
1592. Break it down into logical, manageable subtasks
iomodo5c99a442025-07-28 14:23:52 +04001603. Assign each subtask to the most appropriate team role OR propose creating new agents
iomodod9ff8da2025-07-28 11:42:22 +04001614. Estimate effort and identify dependencies
1625. Provide a clear execution strategy
163
iomodo5c99a442025-07-28 14:23:52 +0400164If you need specialized skills not covered by existing roles, propose new agent creation.
165
iomodod9ff8da2025-07-28 11:42:22 +0400166Respond with a JSON object in this exact format:
167{
168 "analysis_summary": "Brief analysis of the task and approach",
169 "subtasks": [
170 {
171 "title": "Subtask title",
172 "description": "Detailed description of what needs to be done",
173 "priority": "high|medium|low",
174 "assigned_to": "role_name",
175 "estimated_hours": 8,
iomodo5c99a442025-07-28 14:23:52 +0400176 "dependencies": ["subtask_index_1", "subtask_index_2"],
177 "required_skills": ["skill1", "skill2"]
178 }
179 ],
180 "agent_creations": [
181 {
182 "role": "new_role_name",
183 "skills": ["specialized_skill1", "specialized_skill2"],
184 "description": "Description of what this agent does",
185 "justification": "Why this new agent is needed"
iomodod9ff8da2025-07-28 11:42:22 +0400186 }
187 ],
188 "recommended_approach": "High-level strategy for executing these subtasks",
189 "estimated_total_hours": 40,
190 "risk_assessment": "Potential risks and mitigation strategies"
191}
192
iomodo5c99a442025-07-28 14:23:52 +0400193For existing roles, use: %s
194For new agents, propose appropriate role names and skill sets.
195Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
196}
197
198// buildSubtaskDecisionPrompt creates the user prompt for subtask decision
199func (s *SubtaskService) buildSubtaskDecisionPrompt(task *tm.Task) string {
200 return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
201
202**Task Title:** %s
203
204**Description:** %s
205
206**Priority:** %s
207
208**Current Status:** %s
209
210Consider:
211- Can this be completed by a single agent with existing capabilities?
212- Does it require multiple specialized skills?
213- Is the scope too large for one person?
214- Are there logical components that could be parallelized?
215
216Provide your decision in the JSON format specified in the system prompt.`,
217 task.Title,
218 task.Description,
219 task.Priority,
220 task.Status)
iomodod9ff8da2025-07-28 11:42:22 +0400221}
222
223// buildSubtaskAnalysisPrompt creates the user prompt for LLM analysis
224func (s *SubtaskService) buildSubtaskAnalysisPrompt(task *tm.Task) string {
225 return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
226
227**Task Title:** %s
228
229**Description:** %s
230
231**Priority:** %s
232
233**Current Status:** %s
234
235Please analyze this task and provide a detailed breakdown into subtasks. Consider:
236- Technical complexity and requirements
237- Logical task dependencies
238- Appropriate skill sets needed for each subtask
239- Risk factors and potential blockers
240- Estimated effort for each component
241
242Provide the analysis in the JSON format specified in the system prompt.`,
243 task.Title,
244 task.Description,
245 task.Priority,
246 task.Status)
247}
248
iomodo5c99a442025-07-28 14:23:52 +0400249// parseSubtaskDecision parses the LLM response into a SubtaskDecision struct
250func (s *SubtaskService) parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
251 // Try to extract JSON from the response
252 jsonStart := strings.Index(response, "{")
253 jsonEnd := strings.LastIndex(response, "}")
254
255 if jsonStart == -1 || jsonEnd == -1 {
256 return nil, fmt.Errorf("no JSON found in LLM response")
257 }
258
259 jsonStr := response[jsonStart : jsonEnd+1]
260
261 var decision tm.SubtaskDecision
262 if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
263 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
264 }
265
266 return &decision, nil
267}
268
iomodod9ff8da2025-07-28 11:42:22 +0400269// parseSubtaskAnalysis parses the LLM response into a SubtaskAnalysis struct
270func (s *SubtaskService) parseSubtaskAnalysis(response string, parentTaskID string) (*tm.SubtaskAnalysis, error) {
271 // Try to extract JSON from the response (LLM might wrap it in markdown)
272 jsonStart := strings.Index(response, "{")
273 jsonEnd := strings.LastIndex(response, "}")
274
275 if jsonStart == -1 || jsonEnd == -1 {
276 return nil, fmt.Errorf("no JSON found in LLM response")
277 }
278
279 jsonStr := response[jsonStart : jsonEnd+1]
280
281 var rawAnalysis struct {
282 AnalysisSummary string `json:"analysis_summary"`
283 Subtasks []struct {
284 Title string `json:"title"`
285 Description string `json:"description"`
286 Priority string `json:"priority"`
287 AssignedTo string `json:"assigned_to"`
288 EstimatedHours int `json:"estimated_hours"`
289 Dependencies []string `json:"dependencies"`
iomodo5c99a442025-07-28 14:23:52 +0400290 RequiredSkills []string `json:"required_skills"`
iomodod9ff8da2025-07-28 11:42:22 +0400291 } `json:"subtasks"`
iomodo5c99a442025-07-28 14:23:52 +0400292 AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
iomodod9ff8da2025-07-28 11:42:22 +0400293 RecommendedApproach string `json:"recommended_approach"`
294 EstimatedTotalHours int `json:"estimated_total_hours"`
295 RiskAssessment string `json:"risk_assessment"`
296 }
297
298 if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
299 return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
300 }
301
302 // Convert to our types
303 analysis := &tm.SubtaskAnalysis{
304 ParentTaskID: parentTaskID,
305 AnalysisSummary: rawAnalysis.AnalysisSummary,
iomodo5c99a442025-07-28 14:23:52 +0400306 AgentCreations: rawAnalysis.AgentCreations,
iomodod9ff8da2025-07-28 11:42:22 +0400307 RecommendedApproach: rawAnalysis.RecommendedApproach,
308 EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
309 RiskAssessment: rawAnalysis.RiskAssessment,
310 }
311
312 // Convert subtasks
313 for _, st := range rawAnalysis.Subtasks {
314 priority := tm.PriorityMedium // default
315 switch strings.ToLower(st.Priority) {
316 case "high":
317 priority = tm.PriorityHigh
318 case "low":
319 priority = tm.PriorityLow
320 }
321
322 subtask := tm.SubtaskProposal{
323 Title: st.Title,
324 Description: st.Description,
325 Priority: priority,
326 AssignedTo: st.AssignedTo,
327 EstimatedHours: st.EstimatedHours,
328 Dependencies: st.Dependencies,
iomodo5c99a442025-07-28 14:23:52 +0400329 RequiredSkills: st.RequiredSkills,
iomodod9ff8da2025-07-28 11:42:22 +0400330 }
331
332 analysis.Subtasks = append(analysis.Subtasks, subtask)
333 }
334
iomodo5c99a442025-07-28 14:23:52 +0400335 // Validate agent assignments and handle new agent creation
336 if err := s.validateAndHandleAgentAssignments(analysis); err != nil {
337 log.Printf("Warning during agent assignment handling: %v", err)
iomodod9ff8da2025-07-28 11:42:22 +0400338 }
339
340 return analysis, nil
341}
342
iomodo5c99a442025-07-28 14:23:52 +0400343// validateAndHandleAgentAssignments validates assignments and creates agent creation subtasks if needed
344func (s *SubtaskService) validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis) error {
345 // Collect all agent roles that will be available (existing + proposed new ones)
346 availableRoles := make(map[string]bool)
347 for _, role := range s.agentRoles {
348 availableRoles[role] = true
349 }
350
351 // Add proposed new agent roles
352 for _, agentCreation := range analysis.AgentCreations {
353 availableRoles[agentCreation.Role] = true
354
355 // Create a subtask for agent creation
356 agentCreationSubtask := tm.SubtaskProposal{
357 Title: fmt.Sprintf("Create %s Agent", strings.Title(agentCreation.Role)),
358 Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
359 Priority: tm.PriorityHigh, // Agent creation is high priority
360 AssignedTo: "ceo", // CEO creates new agents
361 EstimatedHours: 4, // Estimated time to set up new agent
362 Dependencies: []string{}, // No dependencies for agent creation
363 RequiredSkills: []string{"agent_configuration", "system_design"},
364 }
365
366 // Insert at the beginning so agent creation happens first
367 analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
368
369 // Update dependencies to account for the new subtask at index 0
370 for i := 1; i < len(analysis.Subtasks); i++ {
371 for j, dep := range analysis.Subtasks[i].Dependencies {
372 // Convert dependency index and increment by 1
373 if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
374 analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
375 }
376 }
iomodod9ff8da2025-07-28 11:42:22 +0400377 }
378 }
iomodo5c99a442025-07-28 14:23:52 +0400379
380 // Now validate all assignments against available roles
iomodod9ff8da2025-07-28 11:42:22 +0400381 defaultRole := "ceo" // fallback role
382 if len(s.agentRoles) > 0 {
383 defaultRole = s.agentRoles[0]
384 }
385
386 for i := range analysis.Subtasks {
iomodo5c99a442025-07-28 14:23:52 +0400387 if !availableRoles[analysis.Subtasks[i].AssignedTo] {
388 log.Printf("Warning: Unknown agent role '%s' for subtask '%s', assigning to %s",
389 analysis.Subtasks[i].AssignedTo, analysis.Subtasks[i].Title, defaultRole)
iomodod9ff8da2025-07-28 11:42:22 +0400390 analysis.Subtasks[i].AssignedTo = defaultRole
391 }
392 }
iomodo5c99a442025-07-28 14:23:52 +0400393
394 return nil
395}
396
397// parseDependencyIndex parses a dependency string to an integer index
398func (s *SubtaskService) parseDependencyIndex(dep string) int {
399 var idx int
400 if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
401 return idx
402 }
403 return -1 // Invalid dependency format
iomodod9ff8da2025-07-28 11:42:22 +0400404}
405
406// isValidAgentRole checks if a role is in the available agent roles
407func (s *SubtaskService) isValidAgentRole(role string) bool {
408 for _, availableRole := range s.agentRoles {
409 if availableRole == role {
410 return true
411 }
412 }
413 return false
414}
415
416// GenerateSubtaskPR creates a PR with the proposed subtasks
417func (s *SubtaskService) GenerateSubtaskPR(ctx context.Context, analysis *tm.SubtaskAnalysis) (string, error) {
iomodo443b20a2025-07-28 15:24:05 +0400418 if s.prProvider == nil {
419 return "", fmt.Errorf("PR provider not configured")
420 }
421
422 // Generate branch name for subtask proposal
423 branchName := fmt.Sprintf("subtasks/%s-proposal", analysis.ParentTaskID)
424
425 // Create Git branch and commit subtask proposal
426 if err := s.createSubtaskBranch(ctx, analysis, branchName); err != nil {
427 return "", fmt.Errorf("failed to create subtask branch: %w", err)
428 }
429
430 // Generate PR content
iomodod9ff8da2025-07-28 11:42:22 +0400431 prContent := s.generateSubtaskPRContent(analysis)
iomodo443b20a2025-07-28 15:24:05 +0400432 title := fmt.Sprintf("Subtask Proposal: %s", analysis.ParentTaskID)
433
434 // Create the pull request
435 options := git.PullRequestOptions{
436 Title: title,
437 Description: prContent,
438 HeadBranch: branchName,
439 BaseBranch: "main",
440 Labels: []string{"subtasks", "proposal", "ai-generated"},
441 Draft: false,
442 }
443
444 pr, err := s.prProvider.CreatePullRequest(ctx, options)
445 if err != nil {
446 return "", fmt.Errorf("failed to create PR: %w", err)
447 }
448
449 prURL := fmt.Sprintf("https://github.com/%s/%s/pull/%d", s.githubOwner, s.githubRepo, pr.Number)
iomodod9ff8da2025-07-28 11:42:22 +0400450 log.Printf("Generated subtask proposal PR: %s", prURL)
iomodo443b20a2025-07-28 15:24:05 +0400451
iomodod9ff8da2025-07-28 11:42:22 +0400452 return prURL, nil
453}
454
455// generateSubtaskPRContent creates markdown content for the subtask proposal PR
456func (s *SubtaskService) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
457 var content strings.Builder
458
459 content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
460 content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
461 content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
462 content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
463
464 if analysis.RiskAssessment != "" {
465 content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
466 }
467
468 content.WriteString("## Proposed Subtasks\n\n")
469
470 for i, subtask := range analysis.Subtasks {
471 content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
472 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
473 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
474 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
475
476 if len(subtask.Dependencies) > 0 {
477 deps := strings.Join(subtask.Dependencies, ", ")
478 content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
479 }
480
481 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
482 }
483
484 content.WriteString("---\n")
485 content.WriteString("*Generated by Staff AI Agent System*\n\n")
486 content.WriteString("**Instructions:**\n")
487 content.WriteString("- Review the proposed subtasks\n")
488 content.WriteString("- Approve or request changes\n")
489 content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
490
491 return content.String()
492}
493
iomodo443b20a2025-07-28 15:24:05 +0400494// createSubtaskBranch creates a Git branch with subtask proposal content
495func (s *SubtaskService) createSubtaskBranch(ctx context.Context, analysis *tm.SubtaskAnalysis, branchName string) error {
496 if s.cloneManager == nil {
497 return fmt.Errorf("clone manager not configured")
498 }
499
500 // Get a temporary clone for creating the subtask branch
501 clonePath, err := s.cloneManager.GetAgentClonePath("subtask-service")
502 if err != nil {
503 return fmt.Errorf("failed to get clone path: %w", err)
504 }
505
506 // All Git operations use the clone directory
507 gitCmd := func(args ...string) *exec.Cmd {
508 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
509 }
510
511 // Ensure we're on main branch before creating new branch
512 cmd := gitCmd("checkout", "main")
513 if err := cmd.Run(); err != nil {
514 // Try master branch if main doesn't exist
515 cmd = gitCmd("checkout", "master")
516 if err := cmd.Run(); err != nil {
517 return fmt.Errorf("failed to checkout main/master branch: %w", err)
518 }
519 }
520
521 // Pull latest changes
522 cmd = gitCmd("pull", "origin")
523 if err := cmd.Run(); err != nil {
524 log.Printf("Warning: failed to pull latest changes: %v", err)
525 }
526
527 // Create and checkout new branch
528 cmd = gitCmd("checkout", "-b", branchName)
529 if err := cmd.Run(); err != nil {
530 return fmt.Errorf("failed to create branch: %w", err)
531 }
532
533 // Create subtask proposal file
534 proposalDir := filepath.Join(clonePath, "tasks", "subtasks")
535 if err := os.MkdirAll(proposalDir, 0755); err != nil {
536 return fmt.Errorf("failed to create proposal directory: %w", err)
537 }
538
539 proposalFile := filepath.Join(proposalDir, fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
540 proposalContent := s.generateSubtaskProposalFile(analysis)
541
542 if err := os.WriteFile(proposalFile, []byte(proposalContent), 0644); err != nil {
543 return fmt.Errorf("failed to write proposal file: %w", err)
544 }
545
546 // Stage the file
547 relativeFile := filepath.Join("tasks", "subtasks", fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
548 cmd = gitCmd("add", relativeFile)
549 if err := cmd.Run(); err != nil {
550 return fmt.Errorf("failed to stage files: %w", err)
551 }
552
553 // Commit changes
554 commitMsg := fmt.Sprintf("Subtask proposal for task %s\n\nGenerated by Staff AI Agent System\nProposed %d subtasks with %d new agents",
555 analysis.ParentTaskID, len(analysis.Subtasks), len(analysis.AgentCreations))
556 cmd = gitCmd("commit", "-m", commitMsg)
557 if err := cmd.Run(); err != nil {
558 return fmt.Errorf("failed to commit: %w", err)
559 }
560
561 // Push branch
562 cmd = gitCmd("push", "-u", "origin", branchName)
563 if err := cmd.Run(); err != nil {
564 return fmt.Errorf("failed to push branch: %w", err)
565 }
566
567 log.Printf("Created subtask proposal branch: %s", branchName)
568 return nil
569}
570
571// generateSubtaskProposalFile creates the content for the subtask proposal file
572func (s *SubtaskService) generateSubtaskProposalFile(analysis *tm.SubtaskAnalysis) string {
573 var content strings.Builder
574
575 content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
576 content.WriteString(fmt.Sprintf("**Generated:** %s\n\n", time.Now().Format(time.RFC3339)))
577 content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
578
579 if len(analysis.AgentCreations) > 0 {
580 content.WriteString("## Proposed New Agents\n\n")
581 for i, agent := range analysis.AgentCreations {
582 content.WriteString(fmt.Sprintf("### %d. %s Agent\n", i+1, strings.Title(agent.Role)))
583 content.WriteString(fmt.Sprintf("- **Skills:** %s\n", strings.Join(agent.Skills, ", ")))
584 content.WriteString(fmt.Sprintf("- **Description:** %s\n", agent.Description))
585 content.WriteString(fmt.Sprintf("- **Justification:** %s\n\n", agent.Justification))
586 }
587 }
588
589 content.WriteString("## Proposed Subtasks\n\n")
590 for i, subtask := range analysis.Subtasks {
591 content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
592 content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
593 content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
594 content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
595 if len(subtask.RequiredSkills) > 0 {
596 content.WriteString(fmt.Sprintf("- **Required Skills:** %s\n", strings.Join(subtask.RequiredSkills, ", ")))
597 }
598 if len(subtask.Dependencies) > 0 {
599 content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", strings.Join(subtask.Dependencies, ", ")))
600 }
601 content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
602 }
603
604 content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
605 content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
606
607 if analysis.RiskAssessment != "" {
608 content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
609 }
610
611 content.WriteString("---\n\n")
612 content.WriteString("*This proposal was generated by the Staff AI Agent System. Review and approve to proceed with subtask creation.*\n")
613
614 return content.String()
615}
616
iomodod9ff8da2025-07-28 11:42:22 +0400617// Close cleans up the service
618func (s *SubtaskService) Close() error {
619 if s.llmProvider != nil {
620 return s.llmProvider.Close()
621 }
622 return nil
623}