rdev/internal/claudebox/sdlc.go
jordan 3b35900a2d feat: enterprise worker pool with HTTP sidecar pattern
Implements horizontally-scalable worker pool architecture:
- claudebox-sidecar: HTTP server for Claude Code, git, and SDLC ops
- rdev-worker: standalone worker binary polling rdev-api for tasks
- HTTP client adapter for sidecar communication
- HPA with custom Prometheus metrics for autoscaling
- ServiceMonitor for metrics scraping

Code review fixes applied:
- URL-encode query parameters in GitStatus (Critical #1)
- Remove unused shellQuote function (Critical #2)
- Use stdlib strings.Split/TrimSpace (Critical #3)
- Add version injection via ldflags (Warning #4)
- Add debug logging for swallowed git/sdlc errors (Warning #5, #6)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 16:21:11 -07:00

101 lines
2.4 KiB
Go

package claudebox
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"os/exec"
"strings"
)
// SDLCRunner executes SDLC CLI commands locally.
type SDLCRunner struct {
workDir string
logger *slog.Logger
}
// SDLCRunnerConfig holds configuration for the SDLC runner.
type SDLCRunnerConfig struct {
// WorkDir is the default working directory.
WorkDir string
// Logger is an optional logger for debug output.
Logger *slog.Logger
}
// NewSDLCRunner creates a new SDLC runner.
func NewSDLCRunner(cfg SDLCRunnerConfig) *SDLCRunner {
logger := cfg.Logger
if logger == nil {
logger = slog.Default()
}
return &SDLCRunner{
workDir: cfg.WorkDir,
logger: logger,
}
}
// SDLCResult contains the result of an SDLC command.
type SDLCResult struct {
Success bool
Output string
Data json.RawMessage // Parsed JSON from sdlc --json output
Error error
}
// Run executes an SDLC CLI command.
func (s *SDLCRunner) Run(ctx context.Context, workDir, command string, args []string) *SDLCResult {
result := &SDLCResult{}
// Ensure .sdlc/ is initialized
if err := s.ensureInit(ctx, workDir); err != nil {
// Log but continue - command might still work
s.logger.Debug("sdlc init failed, continuing with command", "error", err, "work_dir", workDir)
}
// Build the command
sdlcArgs := []string{command}
sdlcArgs = append(sdlcArgs, args...)
sdlcArgs = append(sdlcArgs, "--json")
cmd := exec.CommandContext(ctx, "sdlc", sdlcArgs...)
cmd.Dir = workDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
result.Error = fmt.Errorf("%s: %s", err, stderr.String())
result.Output = stdout.String()
return result
}
result.Success = true
result.Output = stdout.String()
// Try to parse JSON output
output := strings.TrimSpace(stdout.String())
if output != "" && (output[0] == '{' || output[0] == '[') {
result.Data = json.RawMessage(output)
}
return result
}
// ensureInit checks if .sdlc/ exists and runs `sdlc init` if it doesn't.
func (s *SDLCRunner) ensureInit(ctx context.Context, workDir string) error {
// Check if .sdlc/ directory exists
cmd := exec.CommandContext(ctx, "test", "-d", workDir+"/.sdlc")
if cmd.Run() == nil {
return nil // Already initialized
}
// Run sdlc init
initCmd := exec.CommandContext(ctx, "sdlc", "init", "--json")
initCmd.Dir = workDir
return initCmd.Run()
}