--json-schema combined with --output-format text produces empty stdout. The structured result is in the "structured_output" field of the json envelope. Updated executor to unwrap the envelope. Also removes --bare flag which disables OAuth keychain reads, causing silent auth failure when ANTHROPIC_API_KEY is not set. Adds goreman Procfile + stdio bridge (cmd/bridge) for Claude Code MCP integration. Task start/stop replaced with goreman + port-kill. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
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",
|
|
"--permission-mode", "bypassPermissions",
|
|
"--tools", tools,
|
|
"--json-schema", Schema,
|
|
"--output-format", "json",
|
|
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())
|
|
}
|
|
|
|
// --output-format json wraps the response in an envelope; structured output
|
|
// from --json-schema is in the "structured_output" field.
|
|
var envelope struct {
|
|
StructuredOutput *Result `json:"structured_output"`
|
|
IsError bool `json:"is_error"`
|
|
Result string `json:"result"` // fallback text result for error messages
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
return Result{}, fmt.Errorf("parse envelope JSON: %w — raw: %s — stderr: %s", err, stdout.String(), stderr.String())
|
|
}
|
|
if envelope.StructuredOutput == nil {
|
|
return Result{}, fmt.Errorf("no structured_output in response — result: %s — stderr: %s", envelope.Result, stderr.String())
|
|
}
|
|
if err := envelope.StructuredOutput.Validate(); err != nil {
|
|
return Result{}, fmt.Errorf("invalid result: %w", err)
|
|
}
|
|
return *envelope.StructuredOutput, nil
|
|
}
|