package service import ( "context" "fmt" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/sdlc" ) // GitCommitResult describes the outcome of a git commit+push operation. type GitCommitResult struct { HasChanges bool CommitSHA string FilesChanged []string Pushed bool Error error } // PodGitCommitter abstracts git commit/push operations in pods. // This avoids importing internal/worker directly, preventing import cycles. type PodGitCommitter interface { CommitAndPush(ctx context.Context, podName, workDir, message string, push bool) *GitCommitResult } // SDLCOrchestratorService orchestrates SDLC actions: execute, resolve, commit. type SDLCOrchestratorService struct { sdlcService *SDLCService agentRegistry port.CodeAgentRegistry gitCommitter PodGitCommitter projectRepo port.ProjectRepository } // NewSDLCOrchestratorService creates a new orchestrator service. func NewSDLCOrchestratorService( sdlcService *SDLCService, agentRegistry port.CodeAgentRegistry, gitCommitter PodGitCommitter, projectRepo port.ProjectRepository, ) *SDLCOrchestratorService { return &SDLCOrchestratorService{ sdlcService: sdlcService, agentRegistry: agentRegistry, gitCommitter: gitCommitter, projectRepo: projectRepo, } } // ExecuteRequest describes what action to execute. type ExecuteRequest struct { Feature string `json:"feature"` Provider string `json:"provider,omitempty"` } // ExecutionResult is the result of executing an SDLC action. type ExecutionResult struct { Action sdlc.ActionType `json:"action"` Success bool `json:"success"` Output string `json:"output,omitempty"` Next *sdlc.Classification `json:"next,omitempty"` Error string `json:"error,omitempty"` } // ResolveRequest describes a blocker resolution. type ResolveRequest struct { Feature string `json:"feature"` } // CommitRequest describes a commit operation. type CommitRequest struct { Feature string `json:"feature"` Message string `json:"message"` Push bool `json:"push"` } // CommitResult is the result of a commit operation. type CommitResult struct { CommitSHA string `json:"commit_sha,omitempty"` FilesChanged []string `json:"files_changed,omitempty"` Pushed bool `json:"pushed"` } // ExecuteAction gets the next classifier action and executes it. func (s *SDLCOrchestratorService) ExecuteAction(ctx context.Context, projectID string, req *ExecuteRequest) (*ExecutionResult, error) { cl, err := s.sdlcService.GetNext(ctx, projectID, req.Feature) if err != nil { return nil, err } result := &ExecutionResult{ Action: cl.Action, } switch cl.Action { case sdlc.ActionTransition: err = s.executeTransition(ctx, projectID, cl) case sdlc.ActionIdle, sdlc.ActionBlocked, sdlc.ActionAwaitApproval: result.Output = cl.Message result.Success = true default: err = s.executeAgentAction(ctx, projectID, cl, req, result) } if err != nil { result.Error = err.Error() return result, nil } if result.Error == "" { result.Success = true } next, nextErr := s.sdlcService.GetNext(ctx, projectID, req.Feature) if nextErr == nil { result.Next = next } return result, nil } func (s *SDLCOrchestratorService) executeTransition(ctx context.Context, projectID string, cl *sdlc.Classification) error { log := logging.FromContext(ctx).WithService("sdlc_orchestrator") log.Info("executing transition", logging.FieldProjectID, projectID, "feature", cl.Feature, "to_phase", string(cl.TransitionTo), ) return s.sdlcService.TransitionFeature(ctx, projectID, cl.Feature, cl.TransitionTo) } func (s *SDLCOrchestratorService) executeAgentAction(ctx context.Context, projectID string, cl *sdlc.Classification, req *ExecuteRequest, result *ExecutionResult) error { var agent port.CodeAgent if req.Provider != "" { provider, err := domain.ParseAgentProvider(req.Provider) if err != nil { return fmt.Errorf("invalid agent provider: %w", err) } agent = s.agentRegistry.Get(provider) } else { agent = s.agentRegistry.Default() } if agent == nil { return fmt.Errorf("no agent available") } project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { return fmt.Errorf("resolve project: %w", err) } prompt := cl.NextCommand if prompt == "" { prompt = fmt.Sprintf("Execute SDLC action: %s for feature %s", cl.Action, cl.Feature) } agentReq := &domain.AgentRequest{ Prompt: prompt, ProjectID: project.ID, Timeout: 10 * time.Minute, Metadata: map[string]string{ "pod_name": project.PodName, "namespace": "rdev", "action": string(cl.Action), "feature": cl.Feature, }, } log := logging.FromContext(ctx).WithService("sdlc_orchestrator") log.Info("dispatching agent action", logging.FieldProjectID, projectID, "feature", cl.Feature, "action", string(cl.Action), "agent", agent.Name(), ) var output string _, err = agent.Execute(ctx, agentReq, func(event domain.AgentEvent) { if event.Type == domain.AgentEventOutput { output += event.Content } }) if err != nil { return fmt.Errorf("agent execution failed: %w", err) } result.Output = output return nil } // ResolveBlocker unblocks a feature and re-classifies. func (s *SDLCOrchestratorService) ResolveBlocker(ctx context.Context, projectID string, req *ResolveRequest) (*ExecutionResult, error) { if err := s.sdlcService.UnblockFeature(ctx, projectID, req.Feature); err != nil { return nil, err } log := logging.FromContext(ctx).WithService("sdlc_orchestrator") log.Info("blocker resolved", logging.FieldProjectID, projectID, "feature", req.Feature) cl, err := s.sdlcService.GetNext(ctx, projectID, req.Feature) if err != nil { return &ExecutionResult{ Action: sdlc.ActionTransition, Success: true, Output: "Feature unblocked", }, nil } return &ExecutionResult{ Action: sdlc.ActionTransition, Success: true, Output: "Feature unblocked", Next: cl, }, nil } // CommitChanges commits and optionally pushes changes in the project pod. func (s *SDLCOrchestratorService) CommitChanges(ctx context.Context, projectID string, req *CommitRequest) (*CommitResult, error) { if s.gitCommitter == nil { return nil, fmt.Errorf("git operations not configured") } project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { return nil, domain.ErrProjectNotFound } workDir := "/workspace" if project.Workspace != "" { workDir = project.Workspace } gitResult := s.gitCommitter.CommitAndPush(ctx, project.PodName, workDir, req.Message, req.Push) if gitResult.Error != nil { return nil, gitResult.Error } log := logging.FromContext(ctx).WithService("sdlc_orchestrator") log.Info("changes committed", logging.FieldProjectID, projectID, "sha", gitResult.CommitSHA, "files", len(gitResult.FilesChanged), "pushed", gitResult.Pushed, ) return &CommitResult{ CommitSHA: gitResult.CommitSHA, FilesChanged: gitResult.FilesChanged, Pushed: gitResult.Pushed, }, nil }