Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cmd/roborev/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ To fix an existing analysis job, use: roborev fix <job_id>

cmd.Flags().StringVar(&agentName, "agent", "", "agent to use for analysis (default: from config)")
cmd.Flags().StringVar(&model, "model", "", "model for analysis agent")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, or thorough")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, medium, thorough, or maximum")
cmd.Flags().BoolVar(&wait, "wait", false, "wait for job to complete and show result")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress output (just enqueue)")
cmd.Flags().BoolVar(&listTypes, "list", false, "list available analysis types")
Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Flags override config values. When run inside GitHub ` +
cmd.Flags().StringVar(&reviewTypes, "review-types", "",
"comma-separated review types (overrides config)")
cmd.Flags().StringVar(&reasoning, "reasoning", "",
"reasoning level: thorough, standard, fast")
"reasoning level: fast, standard, medium, thorough, or maximum")
cmd.Flags().StringVar(&minSeverity, "min-severity", "",
"minimum severity filter: critical, high, medium, low")
cmd.Flags().StringVar(&synthesisAgent, "synthesis-agent", "",
Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Examples:

cmd.Flags().StringVar(&agentName, "agent", "", "agent to use for verification (defaults to fix agent from config)")
cmd.Flags().StringVar(&model, "model", "", "model to use")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level (fast/standard/thorough)")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level (fast/standard/medium/thorough/maximum)")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress progress output")
cmd.Flags().BoolVar(&allBranches, "all-branches", false, "include all branches")
cmd.Flags().StringVar(&branch, "branch", "", "filter by branch (default: current branch)")
Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ Examples:

cmd.Flags().StringVar(&agentName, "agent", "", "agent to use for fixes (default: from config)")
cmd.Flags().StringVar(&model, "model", "", "model for agent")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, or thorough")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, medium, thorough, or maximum")
cmd.Flags().StringVar(&minSeverity, "min-severity", "", "minimum finding severity to address: critical, high, medium, or low")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress progress output")
cmd.Flags().BoolVar(&open, "open", false, "fix all open completed jobs for the current repo")
Expand Down
2 changes: 2 additions & 0 deletions cmd/roborev/local_review_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ func TestLocalReviewReasoningLevels(t *testing.T) {
}{
{name: "Fast", reasoning: string(agent.ReasoningFast), expected: "reasoning: " + string(agent.ReasoningFast)},
{name: "Standard", reasoning: string(agent.ReasoningStandard), expected: "reasoning: " + string(agent.ReasoningStandard)},
{name: "Medium", reasoning: string(agent.ReasoningMedium), expected: "reasoning: " + string(agent.ReasoningMedium)},
{name: "Thorough", reasoning: string(agent.ReasoningThorough), expected: "reasoning: " + string(agent.ReasoningThorough)},
{name: "Maximum", reasoning: string(agent.ReasoningMaximum), expected: "reasoning: " + string(agent.ReasoningMaximum)},
{name: "Default", reasoning: "", expected: "reasoning: " + string(agent.ReasoningThorough)}, // default (agent defaults)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/refine.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Use --all-branches to discover and refine all branches with failed reviews.`,

cmd.Flags().StringVar(&opts.agentName, "agent", "", "agent to use for addressing findings (default: from config)")
cmd.Flags().StringVar(&opts.model, "model", "", "model for agent (format varies: opencode uses provider/model, others use model name)")
cmd.Flags().StringVar(&opts.reasoning, "reasoning", "", "reasoning level: fast, standard (default), or thorough")
cmd.Flags().StringVar(&opts.reasoning, "reasoning", "", "reasoning level: fast, standard (default), medium, thorough, or maximum")
cmd.Flags().StringVar(&opts.minSeverity, "min-severity", "", "minimum finding severity to address: critical, high, medium, or low")
cmd.Flags().BoolVar(&fast, "fast", false, "shorthand for --reasoning fast")
cmd.Flags().IntVar(&opts.maxIterations, "max-iterations", 10, "maximum refinement iterations")
Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ Examples:
cmd.Flags().StringVar(&sha, "sha", "HEAD", "commit SHA to review (used when no positional args)")
cmd.Flags().StringVar(&agent, "agent", "", "agent to use (codex, claude-code, gemini, copilot, opencode, cursor, kiro, kilo, pi)")
cmd.Flags().StringVar(&model, "model", "", "model for agent (format varies: opencode uses provider/model, others use model name)")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: thorough (default), standard, or fast")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, medium, thorough (default), or maximum")
cmd.Flags().BoolVar(&fast, "fast", false, "shorthand for --reasoning fast")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress output (for use in hooks)")
cmd.Flags().BoolVar(&dirty, "dirty", false, "review uncommitted changes instead of a commit")
Expand Down
2 changes: 1 addition & 1 deletion cmd/roborev/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Examples:

cmd.Flags().StringVar(&agentName, "agent", "", "agent to use (default: from config)")
cmd.Flags().StringVar(&model, "model", "", "model for agent (format varies: opencode uses provider/model, others use model name)")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, or thorough (default)")
cmd.Flags().StringVar(&reasoning, "reasoning", "", "reasoning level: fast, standard, medium, thorough (default), or maximum")
cmd.Flags().BoolVar(&wait, "wait", false, "wait for job to complete and show result")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress output (just enqueue)")
cmd.Flags().BoolVar(&noContext, "no-context", false, "don't include repository context in prompt")
Expand Down
16 changes: 12 additions & 4 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,35 @@ import (
type ReasoningLevel string

const (
// ReasoningThorough uses maximum reasoning for deep analysis (slower)
// ReasoningMaximum uses the highest available reasoning (e.g., codex xhigh, claude max)
ReasoningMaximum ReasoningLevel = "maximum"
// ReasoningThorough uses deep reasoning for thorough analysis (slower)
ReasoningThorough ReasoningLevel = "thorough"
// ReasoningStandard uses balanced reasoning (default)
// ReasoningMedium uses moderate reasoning (e.g., claude --effort medium)
ReasoningMedium ReasoningLevel = "medium"
// ReasoningStandard uses the model's default reasoning (no effort override)
ReasoningStandard ReasoningLevel = "standard"
// ReasoningFast uses minimal reasoning for quick responses
ReasoningFast ReasoningLevel = "fast"
)

// ReasoningLevels returns the canonical reasoning level names.
func ReasoningLevels() []string {
return []string{string(ReasoningFast), string(ReasoningStandard), string(ReasoningThorough)}
return []string{string(ReasoningFast), string(ReasoningStandard), string(ReasoningMedium), string(ReasoningThorough), string(ReasoningMaximum)}
}

// ParseReasoningLevel converts a string to ReasoningLevel, defaulting to standard
func ParseReasoningLevel(s string) ReasoningLevel {
switch s {
case "maximum", "max", "xhigh":
return ReasoningMaximum
case "thorough", "high":
return ReasoningThorough
case "medium":
return ReasoningMedium
case "fast", "low":
return ReasoningFast
case "standard", "medium", "":
case "standard", "":
return ReasoningStandard
default:
return ReasoningStandard
Expand Down
41 changes: 40 additions & 1 deletion internal/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,15 @@ func TestParseReasoningLevel(t *testing.T) {
input string
want ReasoningLevel
}{
{"maximum", ReasoningMaximum},
{"max", ReasoningMaximum},
{"xhigh", ReasoningMaximum},
{"thorough", ReasoningThorough},
{"high", ReasoningThorough},
{"fast", ReasoningFast},
{"low", ReasoningFast},
{"medium", ReasoningMedium},
{"standard", ReasoningStandard},
{"medium", ReasoningStandard},
{"", ReasoningStandard},
{"unknown", ReasoningStandard},
}
Expand All @@ -155,6 +158,7 @@ func TestCodexReasoningEffortMapping(t *testing.T) {
level ReasoningLevel
want string
}{
{ReasoningMaximum, "xhigh"},
{ReasoningThorough, "high"},
{ReasoningFast, "low"},
{ReasoningStandard, ""},
Expand All @@ -168,6 +172,26 @@ func TestCodexReasoningEffortMapping(t *testing.T) {
}
}

func TestClaudeEffortMapping(t *testing.T) {
tests := []struct {
level ReasoningLevel
want string
}{
{ReasoningMaximum, "max"},
{ReasoningThorough, "high"},
{ReasoningMedium, "medium"},
{ReasoningFast, "low"},
{ReasoningStandard, ""},
}

for _, tt := range tests {
a := NewClaudeAgent("").WithReasoning(tt.level)
claude, ok := a.(*ClaudeAgent)
require.True(t, ok, "expected ClaudeAgent, got %T", a)
assert.Equal(t, tt.want, claude.claudeEffort(), "claudeEffort(%q)", tt.level)
}
}

type agentTestDef struct {
name string
factory func(string) Agent
Expand Down Expand Up @@ -247,6 +271,21 @@ func TestWithModelEmptyPreservesDefault(t *testing.T) {
}
}

func TestClaudeBuildArgsEffort(t *testing.T) {
a := NewClaudeAgent("").WithModel("opus").WithReasoning(ReasoningMaximum)
cmdLine := a.CommandLine()

assert.Contains(t, cmdLine, "--effort max")
assert.Contains(t, cmdLine, "--model opus")
}

func TestClaudeBuildArgsNoEffortForStandard(t *testing.T) {
a := NewClaudeAgent("").WithReasoning(ReasoningStandard)
cmdLine := a.CommandLine()

assert.NotContains(t, cmdLine, "--effort")
}

func TestAgentBuildArgsWithModel(t *testing.T) {
for _, tt := range agentFixtures {
t.Run(tt.name+" with explicit model", func(t *testing.T) {
Expand Down
48 changes: 43 additions & 5 deletions internal/agent/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import (
type ClaudeAgent struct {
Command string // The claude command to run (default: "claude")
Model string // Model to use (e.g., "opus", "sonnet", or full name)
Reasoning ReasoningLevel // Reasoning level (for future extended thinking support)
Reasoning ReasoningLevel // Reasoning level mapped to --effort flag
Agentic bool // Whether agentic mode is enabled (allow file edits)
SessionID string // Existing session ID to resume
}

const claudeDangerousFlag = "--dangerously-skip-permissions"
const claudeEffortFlag = "--effort"

var claudeDangerousSupport sync.Map
var claudeEffortSupport sync.Map

// NewClaudeAgent creates a new Claude Code agent
func NewClaudeAgent(command string) *ClaudeAgent {
Expand All @@ -35,7 +37,7 @@ func NewClaudeAgent(command string) *ClaudeAgent {
return &ClaudeAgent{Command: command, Reasoning: ReasoningStandard}
}

// WithReasoning returns a copy of the agent with the model preserved (reasoning not yet supported).
// WithReasoning returns a copy of the agent with the specified reasoning level.
func (a *ClaudeAgent) WithReasoning(level ReasoningLevel) Agent {
return &ClaudeAgent{
Command: a.Command,
Expand Down Expand Up @@ -82,6 +84,22 @@ func (a *ClaudeAgent) WithSessionID(sessionID string) Agent {
}
}

// claudeEffort maps ReasoningLevel to Claude Code's --effort flag values
func (a *ClaudeAgent) claudeEffort() string {
switch a.Reasoning {
case ReasoningMaximum:
return "max"
case ReasoningThorough:
return "high"
case ReasoningMedium:
return "medium"
case ReasoningFast:
return "low"
default:
return "" // use claude default (standard = no override)
}
}

func (a *ClaudeAgent) Name() string {
return "claude-code"
}
Expand All @@ -92,11 +110,11 @@ func (a *ClaudeAgent) CommandName() string {

func (a *ClaudeAgent) CommandLine() string {
agenticMode := a.Agentic || AllowUnsafeAgents()
args := a.buildArgs(agenticMode)
args := a.buildArgs(agenticMode, true)
return a.Command + " " + strings.Join(args, " ")
}

func (a *ClaudeAgent) buildArgs(agenticMode bool) []string {
func (a *ClaudeAgent) buildArgs(agenticMode, includeEffort bool) []string {
sessionID := sanitizedResumeSessionID(a.SessionID)
// Always use stdin piping + stream-json for non-interactive execution
// (following claude-code-action pattern from Anthropic)
Expand All @@ -109,6 +127,12 @@ func (a *ClaudeAgent) buildArgs(agenticMode bool) []string {
args = append(args, "--resume", sessionID)
}

if includeEffort {
if effort := a.claudeEffort(); effort != "" {
args = append(args, claudeEffortFlag, effort)
}
}

if agenticMode {
// Agentic mode: Claude can use tools and make file changes
args = append(args, claudeDangerousFlag)
Expand All @@ -134,6 +158,17 @@ func claudeSupportsDangerousFlag(ctx context.Context, command string) (bool, err
return supported, nil
}

func claudeSupportsEffortFlag(ctx context.Context, command string) bool {
if cached, ok := claudeEffortSupport.Load(command); ok {
return cached.(bool)
}
cmd := exec.CommandContext(ctx, command, "--help")
output, _ := cmd.CombinedOutput()
supported := strings.Contains(string(output), claudeEffortFlag)
claudeEffortSupport.Store(command, supported)
return supported
}

func (a *ClaudeAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string, output io.Writer) (string, error) {
// Use agentic mode if either per-job setting or global setting enables it
agenticMode := a.Agentic || AllowUnsafeAgents()
Expand All @@ -148,8 +183,11 @@ func (a *ClaudeAgent) Review(ctx context.Context, repoPath, commitSHA, prompt st
}
}

// Only pass --effort if the installed Claude Code supports it
includeEffort := a.claudeEffort() != "" && claudeSupportsEffortFlag(ctx, a.Command)

// Build args - always uses stdin piping + stream-json for non-interactive execution
args := a.buildArgs(agenticMode)
args := a.buildArgs(agenticMode, includeEffort)

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
Expand Down
8 changes: 4 additions & 4 deletions internal/agent/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestClaudeBuildArgs(t *testing.T) {

t.Run("ReviewMode", func(t *testing.T) {
// Non-agentic mode (review only): read-only tools, no dangerous flag
args := a.buildArgs(false)
args := a.buildArgs(false, false)
for _, req := range []string{"--output-format", "stream-json", "--verbose", "-p", "--allowedTools"} {
assertContainsArg(t, args, req)
}
Expand All @@ -61,7 +61,7 @@ func TestClaudeBuildArgs(t *testing.T) {

t.Run("AgenticMode", func(t *testing.T) {
// Agentic mode: write tools + dangerous flag
args := a.buildArgs(true)
args := a.buildArgs(true, false)
assertContainsArg(t, args, claudeDangerousFlag)
assertContainsArg(t, args, "--allowedTools")

Expand All @@ -71,13 +71,13 @@ func TestClaudeBuildArgs(t *testing.T) {
})

t.Run("ResumeSession", func(t *testing.T) {
args := a.WithSessionID("session-123").(*ClaudeAgent).buildArgs(false)
args := a.WithSessionID("session-123").(*ClaudeAgent).buildArgs(false, false)
assertContainsArg(t, args, "--resume")
assertContainsArg(t, args, "session-123")
})

t.Run("RejectInvalidResumeSession", func(t *testing.T) {
args := a.WithSessionID("-bad-session").(*ClaudeAgent).buildArgs(false)
args := a.WithSessionID("-bad-session").(*ClaudeAgent).buildArgs(false, false)
assertNotContainsArg(t, args, "--resume")
assertNotContainsArg(t, args, "-bad-session")
})
Expand Down
2 changes: 2 additions & 0 deletions internal/agent/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ func (a *CodexAgent) WithSessionID(sessionID string) Agent {
// codexReasoningEffort maps ReasoningLevel to codex-specific effort values
func (a *CodexAgent) codexReasoningEffort() string {
switch a.Reasoning {
case ReasoningMaximum:
return "xhigh"
case ReasoningThorough:
return "high"
case ReasoningFast:
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/droid.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (a *DroidAgent) WithModel(model string) Agent {
// droidReasoningEffort maps ReasoningLevel to droid-specific effort values
func (a *DroidAgent) droidReasoningEffort() string {
switch a.Reasoning {
case ReasoningThorough:
case ReasoningMaximum, ReasoningThorough:
return "high"
case ReasoningFast:
return "low"
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/kilo.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (a *KiloAgent) CommandName() string {
// kiloVariant maps ReasoningLevel to kilo's --variant flag values
func (a *KiloAgent) kiloVariant() string {
switch a.Reasoning {
case ReasoningThorough:
case ReasoningMaximum, ReasoningThorough:
return "high"
case ReasoningFast:
return "minimal"
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (a *PiAgent) buildArgs(repoPath string) []string {

func (a *PiAgent) thinkingLevel() string {
switch a.Reasoning {
case ReasoningThorough:
case ReasoningMaximum, ReasoningThorough:
return "high"
case ReasoningFast:
return "low"
Expand Down
Loading
Loading