rdev/internal/service/sdlc_orchestrator.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

255 lines
7.0 KiB
Go

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
}