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
14 changes: 11 additions & 3 deletions cmd/roborev/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3034,6 +3034,7 @@ func checkAgentsCmd() *cobra.Command {
var (
timeoutSecs int
agentFilter string
largePrompt bool
)

cmd := &cobra.Command{
Expand All @@ -3045,15 +3046,20 @@ For each agent found on PATH, runs a short smoke-test prompt with a timeout
to verify the agent is actually functional.

Examples:
roborev check-agents # Check all agents
roborev check-agents --agent codex # Check only codex
roborev check-agents --timeout 30 # 30 second timeout per agent`,
roborev check-agents # Check all agents
roborev check-agents --agent codex # Check only codex
roborev check-agents --timeout 30 # 30 second timeout per agent
roborev check-agents --large-prompt # Test with 33KB+ prompt (Windows limit check)`,
RunE: func(cmd *cobra.Command, args []string) error {
names := agent.Available()
sort.Strings(names)

timeout := time.Duration(timeoutSecs) * time.Second
smokePrompt := "Respond with exactly: OK"
if largePrompt {
smokePrompt = "Respond with exactly: OK\n" +
strings.Repeat("// padding line\n", 2200)
}

// Use current directory as repo path for the smoke test
repoPath, err := os.Getwd()
Expand Down Expand Up @@ -3124,6 +3130,8 @@ Examples:
cmd.SilenceUsage = true
cmd.Flags().IntVar(&timeoutSecs, "timeout", 60, "timeout in seconds per agent")
cmd.Flags().StringVar(&agentFilter, "agent", "", "check only this agent")
cmd.Flags().BoolVar(&largePrompt, "large-prompt", false,
"use a 33KB+ prompt to test Windows command-line limits")

return cmd
}
Expand Down
2 changes: 2 additions & 0 deletions internal/agent/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"os/exec"
"strings"
"sync"
"time"
)

// ClaudeAgent runs code reviews using Claude Code CLI
Expand Down Expand Up @@ -133,6 +134,7 @@

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.WaitDelay = 5 * time.Second

// Handle API key: use configured key if set, otherwise filter out env var
// to ensure Claude uses subscription auth instead of unexpected API charges
Expand Down Expand Up @@ -207,7 +209,7 @@
if line != "" {
// Stream raw line to the writer for progress visibility
if sw := newSyncWriter(output); sw != nil {
sw.Write([]byte(line + "\n"))

Check failure on line 212 in internal/agent/claude.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `sw.Write` is not checked (errcheck)
}

var msg claudeStreamMessage
Expand Down
2 changes: 2 additions & 0 deletions internal/agent/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"os/exec"
"strings"
"sync"
"time"
)

// CodexAgent runs code reviews using the Codex CLI
Expand Down Expand Up @@ -195,6 +196,7 @@

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.WaitDelay = 5 * time.Second

// Pipe prompt via stdin to avoid command line length limits on Windows.
// Windows has a ~32KB limit on command line arguments, which large diffs easily exceed.
Expand Down Expand Up @@ -310,7 +312,7 @@
if trimmed != "" {
// Stream raw line to the writer for progress visibility
if sw != nil {
sw.Write([]byte(trimmed + "\n"))

Check failure on line 315 in internal/agent/codex.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `sw.Write` is not checked (errcheck)
}

var ev codexEvent
Expand Down
4 changes: 1 addition & 3 deletions internal/agent/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,17 @@ func (a *CopilotAgent) CommandLine() string {
if a.Model != "" {
args = append(args, "--model", a.Model)
}
args = append(args, "--prompt")
return a.Command + " " + strings.Join(args, " ")
}

func (a *CopilotAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string, output io.Writer) (string, error) {
// Use copilot with --prompt for non-interactive mode
args := []string{}
if a.Model != "" {
args = append(args, "--model", a.Model)
}
args = append(args, "--prompt", prompt)

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Stdin = strings.NewReader(prompt)
cmd.Dir = repoPath

var stdout, stderr bytes.Buffer
Expand Down
10 changes: 5 additions & 5 deletions internal/agent/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"strings"
"time"
)

// CursorAgent runs code reviews using the Cursor agent CLI
Expand Down Expand Up @@ -83,7 +84,7 @@ func (a *CursorAgent) CommandLine() string {
return a.Command + " " + strings.Join(args, " ")
}

func (a *CursorAgent) buildArgs(agenticMode bool, prompt string) []string {
func (a *CursorAgent) buildArgs(agenticMode bool) []string {
// -p enables non-interactive print mode (like Claude Code's -p flag)
args := []string{"-p", "--output-format", "stream-json"}

Expand All @@ -100,20 +101,19 @@ func (a *CursorAgent) buildArgs(agenticMode bool, prompt string) []string {
args = append(args, "--mode", "plan")
}

// Prompt is a positional argument for the agent CLI
args = append(args, prompt)

return args
}

func (a *CursorAgent) Review(ctx context.Context, repoPath, commitSHA, prompt string, output io.Writer) (string, error) {
agenticMode := a.Agentic || AllowUnsafeAgents()

args := a.buildArgs(agenticMode, prompt)
args := a.buildArgs(agenticMode)

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.Env = os.Environ()
cmd.WaitDelay = 5 * time.Second
cmd.Stdin = strings.NewReader(prompt)

var stderr bytes.Buffer
stdoutPipe, err := cmd.StdoutPipe()
Expand Down
13 changes: 3 additions & 10 deletions internal/agent/cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func TestCursorBuildArgs(t *testing.T) {
a := NewCursorAgent("agent")

// Non-agentic mode (review): --mode plan, no --force, default model "auto"
args := a.buildArgs(false, "review this")
args := a.buildArgs(false)
assertContainsArg(t, args, "-p")
assertContainsArg(t, args, "--output-format")
assertContainsArg(t, args, "stream-json")
Expand All @@ -19,26 +19,19 @@ func TestCursorBuildArgs(t *testing.T) {
assertContainsArg(t, args, "--mode")
assertContainsArg(t, args, "plan")
assertNotContainsArg(t, args, "--force")
// Prompt should be the last arg
if args[len(args)-1] != "review this" {
t.Errorf("expected prompt as last arg, got %q", args[len(args)-1])
}

// Agentic mode: --force, no --mode plan
args = a.buildArgs(true, "fix this")
args = a.buildArgs(true)
assertContainsArg(t, args, "--force")
assertNotContainsArg(t, args, "--mode")
assertNotContainsArg(t, args, "plan")
if args[len(args)-1] != "fix this" {
t.Errorf("expected prompt as last arg, got %q", args[len(args)-1])
}
}

func TestCursorBuildArgsWithModel(t *testing.T) {
a := NewCursorAgent("agent")
a = a.WithModel("gpt-5.2-codex-high").(*CursorAgent)

args := a.buildArgs(false, "test")
args := a.buildArgs(false)
assertContainsArg(t, args, "--model")
assertContainsArg(t, args, "gpt-5.2-codex-high")
}
Expand Down
9 changes: 3 additions & 6 deletions internal/agent/droid.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (a *DroidAgent) CommandLine() string {
return a.Command + " " + strings.Join(args, " ")
}

func (a *DroidAgent) buildArgs(prompt string, agenticMode bool) []string {
func (a *DroidAgent) buildArgs(agenticMode bool) []string {
args := []string{"exec"}

// Set autonomy level based on agentic mode
Expand All @@ -92,21 +92,18 @@ func (a *DroidAgent) buildArgs(prompt string, agenticMode bool) []string {
args = append(args, "--reasoning-effort", effort)
}

// Add -- to stop flag parsing, then the prompt as the final argument
// This prevents prompts starting with "-" from being parsed as flags
args = append(args, "--", prompt)

return args
}

func (a *DroidAgent) 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()

args := a.buildArgs(prompt, agenticMode)
args := a.buildArgs(agenticMode)

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.Stdin = strings.NewReader(prompt)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
Expand Down
25 changes: 5 additions & 20 deletions internal/agent/droid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,31 @@ func TestDroidBuildArgsAgenticMode(t *testing.T) {
a := NewDroidAgent("droid")

// Test non-agentic mode (--auto low)
args := a.buildArgs("prompt", false)
args := a.buildArgs(false)
assertContainsArg(t, args, "low")
assertNotContainsArg(t, args, "medium")

// Test agentic mode (--auto medium)
args = a.buildArgs("prompt", true)
args = a.buildArgs(true)
assertContainsArg(t, args, "medium")
}

func TestDroidBuildArgsReasoningEffort(t *testing.T) {
// Test thorough reasoning
a := NewDroidAgent("droid").WithReasoning(ReasoningThorough).(*DroidAgent)
args := a.buildArgs("prompt", false)
args := a.buildArgs(false)
assertContainsArg(t, args, "--reasoning-effort")
assertContainsArg(t, args, "high")

// Test fast reasoning
a = NewDroidAgent("droid").WithReasoning(ReasoningFast).(*DroidAgent)
args = a.buildArgs("prompt", false)
args = a.buildArgs(false)
assertContainsArg(t, args, "--reasoning-effort")
assertContainsArg(t, args, "low")

// Test standard reasoning (no flag)
a = NewDroidAgent("droid").WithReasoning(ReasoningStandard).(*DroidAgent)
args = a.buildArgs("prompt", false)
args = a.buildArgs(false)
assertNotContainsArg(t, args, "--reasoning-effort")
}

Expand Down Expand Up @@ -121,21 +121,6 @@ func TestDroidReviewWithProgress(t *testing.T) {
}
}

func TestDroidBuildArgsPromptWithDash(t *testing.T) {
a := NewDroidAgent("droid")

prompt := "-o /tmp/malicious --auto high"
args := a.buildArgs(prompt, false)

// Verify "--" appears before the prompt
assertArgsOrder(t, args, "--", prompt)

// Verify the prompt is passed exactly as last arg
if args[len(args)-1] != prompt {
t.Fatalf("expected prompt as last arg, got %v", args)
}
}

func TestDroidReviewAgenticModeFromGlobal(t *testing.T) {
withUnsafeAgents(t, true)

Expand Down
2 changes: 2 additions & 0 deletions internal/agent/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"os/exec"
"strings"
"time"
)

// errNoStreamJSON indicates no valid stream-json events were parsed.
Expand Down Expand Up @@ -116,6 +117,7 @@ func (a *GeminiAgent) Review(ctx context.Context, repoPath, commitSHA, prompt st

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.WaitDelay = 5 * time.Second

// Pipe prompt via stdin
cmd.Stdin = strings.NewReader(prompt)
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/opencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ func (a *OpenCodeAgent) Review(ctx context.Context, repoPath, commitSHA, prompt
if a.Model != "" {
args = append(args, "--model", a.Model)
}
args = append(args, prompt)

cmd := exec.CommandContext(ctx, a.Command, args...)
cmd.Dir = repoPath
cmd.Stdin = strings.NewReader(prompt)

var stdout, stderr bytes.Buffer
if sw := newSyncWriter(output); sw != nil {
Expand Down
3 changes: 2 additions & 1 deletion internal/daemon/ci_poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1155,8 +1155,9 @@ func (p *CIPoller) postPRComment(ghRepo string, prNumber int, body string) error
cmd := exec.CommandContext(ctx, "gh", "pr", "comment",
"--repo", ghRepo,
fmt.Sprintf("%d", prNumber),
"--body", body,
"--body-file", "-",
)
cmd.Stdin = strings.NewReader(body)
if env := p.ghEnvForRepo(ghRepo); env != nil {
cmd.Env = env
}
Expand Down
Loading