package exec import ( "bytes" "context" "encoding/json" "fmt" "os" "os/exec" "strings" "time" ) // Config holds executor configuration. type Config struct { ClaudeBinary string // path to claude binary, defaults to "claude" SystemPrompt string // contents of supervisor CLAUDE.md Timeout time.Duration // per-invocation timeout, default 120s LiteLLMBaseURL string // passed to Claude so it can delegate to Ollama LiteLLMAPIKey string // passed to Claude for LiteLLM auth } // Request is the input to a single supervisor invocation. type Request struct { SkillPrompt string // skill-specific discipline (e.g. tdd.md contents) TaskPrompt string // the specific task (phase, project_root, spec, model) Model string // resolved model name, passed in task prompt Tools string // comma-separated allowed tools, default "Bash,Read,Write" } // Executor spawns a claude instance and captures its structured JSON output. type Executor struct { cfg Config } func New(cfg Config) *Executor { if cfg.ClaudeBinary == "" { cfg.ClaudeBinary = "claude" } if cfg.Timeout == 0 { cfg.Timeout = 120 * time.Second } return &Executor{cfg: cfg} } func (e *Executor) Run(ctx context.Context, req Request) (Result, error) { ctx, cancel := context.WithTimeout(ctx, e.cfg.Timeout) defer cancel() tools := req.Tools if tools == "" { tools = "Bash,Read,Write" } // Build the full prompt: system rules + skill rules + infra context + task. // LITELLM_API_KEY is injected as a subprocess env var, not in the prompt, // to prevent it appearing in error log output. litellmCtx := fmt.Sprintf("LITELLM_BASE_URL: %s", e.cfg.LiteLLMBaseURL) prompt := strings.Join([]string{ e.cfg.SystemPrompt, "---", req.SkillPrompt, "---", litellmCtx, "---", req.TaskPrompt, }, "\n\n") args := []string{ "--print", "--bare", "--permission-mode", "bypassPermissions", "--tools", tools, "--json-schema", Schema, "--output-format", "text", prompt, } cmd := exec.CommandContext(ctx, e.cfg.ClaudeBinary, args...) cmd.Env = append(os.Environ(), "LITELLM_API_KEY="+e.cfg.LiteLLMAPIKey) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if ctx.Err() != nil { return Result{}, fmt.Errorf("timeout after %s", e.cfg.Timeout) } return Result{}, fmt.Errorf("claude exited with error: %w — stderr: %s", err, stderr.String()) } var r Result if err := json.Unmarshal(stdout.Bytes(), &r); err != nil { return Result{}, fmt.Errorf("parse result JSON: %w — raw output: %s", err, stdout.String()) } if err := r.Validate(); err != nil { return Result{}, fmt.Errorf("invalid result: %w", err) } return r, nil }