fix(exec): use --output-format json to get structured output from claude
--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>
This commit is contained in:
2
Procfile
Normal file
2
Procfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ingestion: cd ingestion && INGEST_BRAIN_DIR=../brain INGEST_PORT=3300 go run ./cmd/server/
|
||||||
|
supervisor: SUPERVISOR_CONFIG_DIR=./config/supervisor SUPERVISOR_MODELS_FILE=./config/models.yaml SUPERVISOR_SESSIONS_DIR=./brain/sessions INGEST_BASE_URL=http://localhost:3300 go run ./cmd/supervisor/
|
||||||
22
Taskfile.yml
22
Taskfile.yml
@@ -66,6 +66,11 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- go build -o bin/supervisor ./cmd/supervisor
|
- go build -o bin/supervisor ./cmd/supervisor
|
||||||
|
|
||||||
|
bridge:build:
|
||||||
|
desc: Build stdio↔HTTP bridge for Claude Code MCP integration
|
||||||
|
cmds:
|
||||||
|
- go build -o bin/supervisor-bridge ./cmd/bridge
|
||||||
|
|
||||||
supervisor:test:smoke:
|
supervisor:test:smoke:
|
||||||
desc: Smoke test supervisor via MCP (requires supervisor:dev running)
|
desc: Smoke test supervisor via MCP (requires supervisor:dev running)
|
||||||
cmds:
|
cmds:
|
||||||
@@ -75,22 +80,15 @@ tasks:
|
|||||||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
|
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
|
||||||
|
|
||||||
start:
|
start:
|
||||||
desc: Start the full hyperguild (ingestion + supervisor) in a tmux session
|
desc: Start ingestion + supervisor (requires goreman — go install github.com/mattn/goreman@latest)
|
||||||
cmds:
|
cmds:
|
||||||
- |
|
- goreman start
|
||||||
tmux new-session -d -s hyperguild -x 220 -y 50 2>/dev/null || true
|
|
||||||
tmux rename-window -t hyperguild:0 'ingestion'
|
|
||||||
tmux send-keys -t hyperguild:ingestion "cd {{.ROOT_DIR}} && INGEST_BRAIN_DIR={{.ROOT_DIR}}/brain INGEST_PORT=3300 go run ./ingestion/cmd/server/" Enter
|
|
||||||
tmux new-window -t hyperguild -n 'supervisor'
|
|
||||||
tmux send-keys -t hyperguild:supervisor "cd {{.ROOT_DIR}} && SUPERVISOR_CONFIG_DIR={{.ROOT_DIR}}/config/supervisor SUPERVISOR_MODELS_FILE={{.ROOT_DIR}}/config/models.yaml SUPERVISOR_SESSIONS_DIR={{.ROOT_DIR}}/brain/sessions INGEST_BASE_URL=http://localhost:3300 go run ./cmd/supervisor/" Enter
|
|
||||||
tmux new-window -t hyperguild -n 'shell'
|
|
||||||
tmux select-window -t hyperguild:shell
|
|
||||||
tmux attach -t hyperguild
|
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
desc: Stop the hyperguild tmux session
|
desc: Stop all hyperguild processes (Ctrl-C in the goreman terminal, or kill by port)
|
||||||
cmds:
|
cmds:
|
||||||
- tmux kill-session -t hyperguild 2>/dev/null || true
|
- lsof -ti:3300 | xargs kill -9 2>/dev/null || true
|
||||||
|
- lsof -ti:3200 | xargs kill -9 2>/dev/null || true
|
||||||
- echo "hyperguild stopped"
|
- echo "hyperguild stopped"
|
||||||
|
|
||||||
ingestion:build:
|
ingestion:build:
|
||||||
|
|||||||
59
cmd/bridge/main.go
Normal file
59
cmd/bridge/main.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// bridge is a stdio↔HTTP adapter that lets Claude Code connect to the
|
||||||
|
// supervisor MCP server via the stdio transport.
|
||||||
|
//
|
||||||
|
// Claude Code spawns this binary as a subprocess and communicates over
|
||||||
|
// stdin/stdout. Each newline-delimited JSON-RPC message from stdin is
|
||||||
|
// forwarded to the supervisor HTTP server and the response is written back.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// SUPERVISOR_URL=http://localhost:3200/mcp bridge
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
url := os.Getenv("SUPERVISOR_URL")
|
||||||
|
if url == "" {
|
||||||
|
url = "http://localhost:3200/mcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
if len(bytes.TrimSpace(line)) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(line))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "bridge: build request: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "bridge: request failed: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
io.Copy(os.Stdout, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
os.Stdout.Write([]byte("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "bridge: scanner: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,11 +68,10 @@ func (e *Executor) Run(ctx context.Context, req Request) (Result, error) {
|
|||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"--print",
|
"--print",
|
||||||
"--bare",
|
|
||||||
"--permission-mode", "bypassPermissions",
|
"--permission-mode", "bypassPermissions",
|
||||||
"--tools", tools,
|
"--tools", tools,
|
||||||
"--json-schema", Schema,
|
"--json-schema", Schema,
|
||||||
"--output-format", "text",
|
"--output-format", "json",
|
||||||
prompt,
|
prompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +88,21 @@ func (e *Executor) Run(ctx context.Context, req Request) (Result, error) {
|
|||||||
return Result{}, fmt.Errorf("claude exited with error: %w — stderr: %s", err, stderr.String())
|
return Result{}, fmt.Errorf("claude exited with error: %w — stderr: %s", err, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var r Result
|
// --output-format json wraps the response in an envelope; structured output
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &r); err != nil {
|
// from --json-schema is in the "structured_output" field.
|
||||||
return Result{}, fmt.Errorf("parse result JSON: %w — raw output: %s", err, stdout.String())
|
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 := r.Validate(); err != nil {
|
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 Result{}, fmt.Errorf("invalid result: %w", err)
|
||||||
}
|
}
|
||||||
return r, nil
|
return *envelope.StructuredOutput, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user