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
3 changes: 3 additions & 0 deletions cmd/roborev/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ func reviewCmd() *cobra.Command {
repoPath string
sha string
agent string
model string
reasoning string
quiet bool
dirty bool
Expand Down Expand Up @@ -748,6 +749,7 @@ Examples:
"repo_path": root,
"git_ref": gitRef,
"agent": agent,
"model": model,
"reasoning": reasoning,
"diff_content": diffContent,
})
Expand Down Expand Up @@ -808,6 +810,7 @@ Examples:
cmd.Flags().StringVar(&repoPath, "repo", "", "path to git repository (default: current directory)")
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)")
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().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
17 changes: 11 additions & 6 deletions cmd/roborev/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func (f failingAgent) Review(ctx context.Context, repoPath, commitSHA, prompt st

func (f failingAgent) WithReasoning(level agent.ReasoningLevel) agent.Agent { return f }
func (f failingAgent) WithAgentic(agentic bool) agent.Agent { return f }
func (f failingAgent) WithModel(model string) agent.Agent { return f }

func TestEnqueueReviewRefine(t *testing.T) {
t.Run("returns job ID on success", func(t *testing.T) {
Expand Down Expand Up @@ -367,7 +368,7 @@ func TestRunRefineSurfacesResponseErrors(t *testing.T) {
}
defer os.Chdir(origDir)

if err := runRefine("test", "", 1, true, false, false, ""); err == nil {
if err := runRefine("test", "", "", 1, true, false, false, ""); err == nil {
t.Fatal("expected error, got nil")
}
}
Expand Down Expand Up @@ -397,7 +398,7 @@ func TestRunRefineQuietNonTTYTimerOutput(t *testing.T) {
defer func() { isTerminal = origIsTerminal }()

output := captureStdout(t, func() {
if err := runRefine("test", "", 1, true, false, false, ""); err == nil {
if err := runRefine("test", "", "", 1, true, false, false, ""); err == nil {
t.Fatal("expected error, got nil")
}
})
Expand Down Expand Up @@ -438,7 +439,7 @@ func TestRunRefineStopsLiveTimerOnAgentError(t *testing.T) {
defer agent.Register(agent.NewTestAgent())

output := captureStdout(t, func() {
if err := runRefine("test", "", 1, true, false, false, ""); err == nil {
if err := runRefine("test", "", "", 1, true, false, false, ""); err == nil {
t.Fatal("expected error, got nil")
}
})
Expand Down Expand Up @@ -484,7 +485,7 @@ func TestRunRefineAgentErrorRetriesWithoutApplyingChanges(t *testing.T) {

output := captureStdout(t, func() {
// With 2 iterations and a failing agent, should exhaust iterations
err := runRefine("test", "", 2, true, false, false, "")
err := runRefine("test", "", "", 2, true, false, false, "")
if err == nil {
t.Fatal("expected error after exhausting iterations, got nil")
}
Expand Down Expand Up @@ -1004,6 +1005,10 @@ func (a *changingAgent) WithAgentic(agentic bool) agent.Agent {
return a
}

func (a *changingAgent) WithModel(model string) agent.Agent {
return a
}

func TestRefineLoopStaysOnFailedFixChain(t *testing.T) {
setupFastPolling(t)
repoDir, _ := setupRefineRepo(t)
Expand Down Expand Up @@ -1192,7 +1197,7 @@ func TestRefineLoopStaysOnFailedFixChain(t *testing.T) {
agent.Register(changer)
defer agent.Register(agent.NewTestAgent())

if err := runRefine("test", "", 2, true, false, false, ""); err == nil {
if err := runRefine("test", "", "", 2, true, false, false, ""); err == nil {
t.Fatal("expected error from reaching max iterations")
}

Expand Down Expand Up @@ -1392,7 +1397,7 @@ func TestRefinePendingJobWaitDoesNotConsumeIteration(t *testing.T) {
// an iteration, this would fail with "max iterations reached". Since the
// pending job transitions to Done with a passing review (and no failed
// reviews exist), refine should succeed.
err = runRefine("test", "", 1, true, false, false, "")
err = runRefine("test", "", "", 1, true, false, false, "")

// Should succeed - all reviews pass after waiting for the pending one
if err != nil {
Expand Down
19 changes: 12 additions & 7 deletions cmd/roborev/refine.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var postCommitWaitDelay = 1 * time.Second
func refineCmd() *cobra.Command {
var (
agentName string
model string
reasoning string
maxIterations int
quiet bool
Expand Down Expand Up @@ -61,11 +62,12 @@ Use --since to specify a starting commit when on the main branch or to
limit how far back to look for reviews to address.`,
RunE: func(cmd *cobra.Command, args []string) error {
unsafeFlagChanged := cmd.Flags().Changed("allow-unsafe-agents")
return runRefine(agentName, reasoning, maxIterations, quiet, allowUnsafeAgents, unsafeFlagChanged, since)
return runRefine(agentName, model, reasoning, maxIterations, quiet, allowUnsafeAgents, unsafeFlagChanged, since)
},
}

cmd.Flags().StringVar(&agentName, "agent", "", "agent to use for addressing findings (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 (default), or thorough")
cmd.Flags().IntVar(&maxIterations, "max-iterations", 10, "maximum refinement iterations")
cmd.Flags().BoolVar(&quiet, "quiet", false, "suppress agent output, show elapsed time instead")
Expand Down Expand Up @@ -185,7 +187,7 @@ func validateRefineContext(since string) (repoPath, currentBranch, defaultBranch
return repoPath, currentBranch, defaultBranch, mergeBase, nil
}

func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, allowUnsafeAgents bool, unsafeFlagChanged bool, since string) error {
func runRefine(agentName, modelStr, reasoningStr string, maxIterations int, quiet bool, allowUnsafeAgents bool, unsafeFlagChanged bool, since string) error {
// 1. Validate git and branch context (before touching daemon)
repoPath, currentBranch, defaultBranch, mergeBase, err := validateRefineContext(since)
if err != nil {
Expand Down Expand Up @@ -225,8 +227,11 @@ func runRefine(agentName, reasoningStr string, maxIterations int, quiet bool, al
}
reasoningLevel := agent.ParseReasoningLevel(resolvedReasoning)

// Get the agent with configured reasoning level
addressAgent, err := selectRefineAgent(resolvedAgent, reasoningLevel)
// Resolve model from CLI or config
resolvedModel := config.ResolveModel(modelStr, repoPath, cfg)

// Get the agent with configured reasoning level and model
addressAgent, err := selectRefineAgent(resolvedAgent, reasoningLevel, resolvedModel)
if err != nil {
return fmt.Errorf("no agent available: %w", err)
}
Expand Down Expand Up @@ -777,18 +782,18 @@ func applyWorktreeChanges(repoPath, worktreePath string) error {
return nil
}

func selectRefineAgent(resolvedAgent string, reasoningLevel agent.ReasoningLevel) (agent.Agent, error) {
func selectRefineAgent(resolvedAgent string, reasoningLevel agent.ReasoningLevel, model string) (agent.Agent, error) {
if resolvedAgent == "codex" && agent.IsAvailable("codex") {
baseAgent, err := agent.Get("codex")
if err != nil {
return nil, err
}
return baseAgent.WithReasoning(reasoningLevel), nil
return baseAgent.WithReasoning(reasoningLevel).WithModel(model), nil
}

baseAgent, err := agent.GetAvailable(resolvedAgent)
if err != nil {
return nil, err
}
return baseAgent.WithReasoning(reasoningLevel), nil
return baseAgent.WithReasoning(reasoningLevel).WithModel(model), nil
}
6 changes: 3 additions & 3 deletions cmd/roborev/refine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ var _ daemon.Client = (*mockDaemonClient)(nil)
func TestSelectRefineAgentCodexFallback(t *testing.T) {
t.Setenv("PATH", "")

selected, err := selectRefineAgent("codex", agent.ReasoningFast)
selected, err := selectRefineAgent("codex", agent.ReasoningFast, "")
if err != nil {
t.Fatalf("selectRefineAgent failed: %v", err)
}
Expand Down Expand Up @@ -234,7 +234,7 @@ func TestSelectRefineAgentCodexUsesRequestedReasoning(t *testing.T) {

t.Setenv("PATH", tmpDir)

selected, err := selectRefineAgent("codex", agent.ReasoningFast)
selected, err := selectRefineAgent("codex", agent.ReasoningFast, "")
if err != nil {
t.Fatalf("selectRefineAgent failed: %v", err)
}
Expand Down Expand Up @@ -267,7 +267,7 @@ func TestSelectRefineAgentCodexFallbackUsesRequestedReasoning(t *testing.T) {
t.Setenv("PATH", tmpDir)

// Request an unavailable agent (claude), codex should be used as fallback
selected, err := selectRefineAgent("claude", agent.ReasoningThorough)
selected, err := selectRefineAgent("claude", agent.ReasoningThorough, "")
if err != nil {
t.Fatalf("selectRefineAgent failed: %v", err)
}
Expand Down
7 changes: 5 additions & 2 deletions cmd/roborev/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
func runCmd() *cobra.Command {
var (
agentName string
model string
reasoning string
wait bool
quiet bool
Expand Down Expand Up @@ -58,11 +59,12 @@ Examples:
cat instructions.txt | roborev run --wait
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runPrompt(cmd, args, agentName, reasoning, wait, quiet, !noContext, agentic)
return runPrompt(cmd, args, agentName, model, reasoning, wait, quiet, !noContext, agentic)
},
}

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().BoolVar(&wait, "wait", false, "wait for job to complete and show result")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "suppress output (just enqueue)")
Expand All @@ -81,7 +83,7 @@ func promptCmd() *cobra.Command {
return cmd
}

func runPrompt(cmd *cobra.Command, args []string, agentName, reasoningStr string, wait, quiet, includeContext, agentic bool) error {
func runPrompt(cmd *cobra.Command, args []string, agentName, modelStr, reasoningStr string, wait, quiet, includeContext, agentic bool) error {
// Get prompt from args or stdin
var promptText string
if len(args) > 0 {
Expand Down Expand Up @@ -136,6 +138,7 @@ func runPrompt(cmd *cobra.Command, args []string, agentName, reasoningStr string
"repo_path": repoRoot,
"git_ref": "run",
"agent": agentName,
"model": modelStr,
"reasoning": reasoningStr,
"custom_prompt": fullPrompt,
"agentic": agentic,
Expand Down
2 changes: 1 addition & 1 deletion e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestDatabaseIntegration(t *testing.T) {
t.Fatalf("GetOrCreateCommit failed: %v", err)
}

job, err := db.EnqueueJob(repo.ID, commit.ID, "abc123", "codex", "")
job, err := db.EnqueueJob(repo.ID, commit.ID, "abc123", "codex", "", "")
if err != nil {
t.Fatalf("EnqueueJob failed: %v", err)
}
Expand Down
5 changes: 5 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ type Agent interface {
// In agentic mode, agents can edit files and run commands.
// If false, agents operate in read-only review mode.
WithAgentic(agentic bool) Agent

// WithModel returns a copy of the agent configured to use the specified model.
// Agents that don't support model selection may return themselves unchanged.
// For opencode, the model format is "provider/model" (e.g., "anthropic/claude-sonnet-4-20250514").
WithModel(model string) Agent
}

// CommandAgent is an agent that uses an external command
Expand Down
Loading
Loading