Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ The redesign eliminated several constructs from the prior implementation. None s
- `--track-only` flag (intentionally removed by #1009)
- `--postreview` / `--finalize` / empty review commits / `/entire-review:finish` skill installer
- `Launcher` + `HeadlessLauncher` as separate interfaces (single `AgentReviewer`)
- `filterCodexOutput` in shared multi-agent code (lives in codex's adapter)
- Codex chrome-line filtering or any agent-specific stdout post-processing in shared multi-agent code (per-agent parsers own their format; shared code only sees `Event` variants)
- `sync.Once`-guarded onCancel + parallel `signal.Notify` goroutine (single cancel from start)

#### Key Files
Expand All @@ -772,7 +772,7 @@ The redesign eliminated several constructs from the prior implementation. None s
- `cmd/entire/cli/review/synthesis_sink.go` / `synthesis_prompt.go` — opt-in cross-agent verdict
- `cmd/entire/cli/review/types/{reviewer,sink,template}.go` — interface contracts (CU2 + CU4 + CU5b)
- `cmd/entire/cli/review/env.go` — `ENTIRE_REVIEW_*` constants + `EncodeSkills`/`DecodeSkills` + `AppendReviewEnv`
- `cmd/entire/cli/agent/{claudecode,codex,geminicli}/reviewer.go` — per-agent `AgentReviewer` implementations (claude-code, codex with chrome filter, gemini-cli)
- `cmd/entire/cli/agent/{claudecode,codex,geminicli}/reviewer.go` — per-agent `AgentReviewer` implementations (claude-code, codex, gemini-cli)
- `cmd/entire/cli/agent/claudecode/discovery.go` — skill discovery + `pickLatestVersion` plugin-cache dedupe
- `cmd/entire/cli/lifecycle.go` — `adoptReviewEnv` reads `ENTIRE_REVIEW_*` from process env; replaces marker-file adoption
- `cmd/entire/cli/review_bridge.go` / `review_helpers.go` — bridge code in `cli` package for cycle-bound functions (`headHasReviewCheckpoint`, `launchableReviewerFor`, `newReviewAttachCmd`, `lazySynthesisProvider`)
Expand Down
108 changes: 94 additions & 14 deletions cmd/entire/cli/agent/claudecode/reviewer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package claudecode
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -12,11 +13,18 @@ import (
reviewtypes "github.com/entireio/cli/cmd/entire/cli/review/types"
)

// envelopeTypeAssistant is the stream-json envelope type for assistant
// messages (per-content-block events). Shared with transcript.go's usage.
const envelopeTypeAssistant = "assistant"

// NewReviewer returns the AgentReviewer for claude-code.
//
// Argv shape: claude -p <prompt>. Plain-text stdout.
// Argv shape: claude -p <prompt> --output-format stream-json --verbose.
// The prompt is passed as a command-line argument; stdin is unused.
// Stdout in -p mode is the assistant's plain-text response (no JSON envelope).
// Stdout is newline-delimited JSON envelopes (one event per line), which the
// parser decodes into the review Event stream. This format gives the parser
// per-message granularity (each assistant content block surfaces as it is
// produced) instead of buffering until end-of-run like plain-text -p mode.
func NewReviewer() *reviewtypes.ReviewerTemplate {
return &reviewtypes.ReviewerTemplate{
AgentName: "claude-code",
Expand All @@ -29,38 +37,110 @@ func NewReviewer() *reviewtypes.ReviewerTemplate {
// Exposed at package level for test inspection of argv and env.
func buildReviewCmd(ctx context.Context, cfg reviewtypes.RunConfig) *exec.Cmd {
prompt := review.ComposeReviewPrompt(cfg)
cmd := exec.CommandContext(ctx, "claude", "-p", prompt)
cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--output-format", "stream-json", "--verbose")
cmd.Env = review.AppendReviewEnv(os.Environ(), "claude-code", cfg, prompt)
return cmd
}

// parseClaudeOutput converts claude's -p mode stdout into a stream of Events.
// In -p mode claude emits the assistant's response as plain text (one line per
// stdout line). The parser emits Started once, then one AssistantText per
// non-empty line, then Finished{Success: true} on clean EOF or
// RunError + Finished{Success: false} on a torn stream (scanner error).
// parseClaudeOutput converts claude's --output-format stream-json --verbose
// stdout into a stream of Events. Each stdout line is one JSON envelope:
// - {"type":"system",...} session metadata / hooks; swallowed
// - {"type":"assistant",...} per content block: text → AssistantText,
// tool_use → ToolCall, thinking → swallowed
// - {"type":"user",...} tool_result echoes; swallowed
// - {"type":"result",...} final summary; emits Tokens then Finished
//
// Emits Started first, Finished{Success:...} last (success follows result.is_error).
// On a scanner error (torn stream), emits RunError then Finished{Success:false}.
//
// Exposed for golden-file contract testing.
// Tokens are emitted only at the terminal `result` envelope, not
// incrementally — claude's per-assistant `usage` fields aren't cumulative
// and summing them across messages would double-count.
//
// Package-private; called directly from this package's tests so they can
// drive raw stdout fixtures through the parser without going through the
// ReviewerTemplate.Start spawn path.
func parseClaudeOutput(r io.Reader) <-chan reviewtypes.Event {
out := make(chan reviewtypes.Event, 32)
go func() {
defer close(out)
out <- reviewtypes.Started{}
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 1024*1024), 16*1024*1024)
// 64MB max line. Claude's stream-json envelopes are small per-message
// in practice, but we share the cap with codex (which can pack large
// command stdout into aggregated_output) so both parsers tolerate the
// same worst case. One buffer per active review run; memory cost is
// modest.
scanner.Buffer(make([]byte, 1024*1024), 64*1024*1024)
var sawResult bool
var resultErr bool
var resultUsage messageUsage
for scanner.Scan() {
line := scanner.Text()
if line == "" {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
out <- reviewtypes.AssistantText{Text: line}
var env claudeEnvelope
if err := json.Unmarshal(line, &env); err != nil {
out <- reviewtypes.RunError{Err: fmt.Errorf("claude stream-json: %w", err)}
continue
}
switch env.Type {
case envelopeTypeAssistant:
for _, block := range env.Message.Content {
switch block.Type {
case "text":
if block.Text != "" {
out <- reviewtypes.AssistantText{Text: block.Text}
}
case "tool_use":
// block.Input is a json.RawMessage; passing it through as a
// string preserves the agent-defined shape without a
// re-marshal round trip. Empty input becomes "" so consumers
// see a falsy Args.
out <- reviewtypes.ToolCall{Name: block.Name, Args: string(block.Input)}
}
}
case "result":
sawResult = true
resultErr = env.IsError
resultUsage = env.Usage
}
}
if err := scanner.Err(); err != nil {
out <- reviewtypes.RunError{Err: fmt.Errorf("read stdout: %w", err)}
out <- reviewtypes.Finished{Success: false}
return
}
out <- reviewtypes.Finished{Success: true}
if sawResult {
in := resultUsage.InputTokens + resultUsage.CacheReadInputTokens + resultUsage.CacheCreationInputTokens
out <- reviewtypes.Tokens{In: in, Out: resultUsage.OutputTokens}
out <- reviewtypes.Finished{Success: !resultErr}
return
}
out <- reviewtypes.Finished{Success: false}
}()
return out
}

type claudeEnvelope struct {
Type string `json:"type"`
Message claudeMessage `json:"message"`
IsError bool `json:"is_error"`
// Usage reuses the package-local messageUsage type (declared in types.go)
// rather than a duplicate ad-hoc struct, so the two consumers of the
// Claude API usage shape (transcript parsing + stream-json review parser)
// can't drift apart.
Usage messageUsage `json:"usage"`
}

type claudeMessage struct {
Content []claudeBlock `json:"content"`
}

type claudeBlock struct {
Type string `json:"type"`
Text string `json:"text"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
}
Loading
Loading