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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ coverage.out

# IDE
.idea/
.cursor/
.vscode/
*.swp
*.swo
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ You catch problems while context is fresh instead of waiting for PR review.
- **Code Analysis** - Built-in analysis types (duplication, complexity,
refactoring, test fixtures, dead code) that agents can fix automatically.
- **Multi-Agent** - Works with Codex, Claude Code, Gemini, Copilot,
OpenCode, Cursor, and Droid.
OpenCode, Cursor, Droid, and Ollama.
- **Runs Locally** - No hosted service or additional infrastructure.
Reviews are orchestrated on your machine using the coding agents
you already have configured.
Expand Down Expand Up @@ -147,6 +147,16 @@ Project-specific review instructions here.
"""
```

**Ollama configuration example:**

```toml
agent = "ollama"
model = "qwen2.5-coder:32b"

# Optional: configure Ollama server URL (default: http://localhost:11434)
# ollama_base_url = "http://localhost:11434"
```

See [configuration guide](https://roborev.io/configuration/) for all options.

## Hooks
Expand Down Expand Up @@ -188,6 +198,7 @@ See [hooks guide](https://roborev.io/guides/hooks/) for details.
| Copilot | `npm install -g @github/copilot` |
| OpenCode | `npm install -g opencode-ai` |
| Cursor | [cursor.com](https://www.cursor.com/) |
| Ollama | `ollama serve` (must be running) |
| Droid | [factory.ai](https://factory.ai/) |

roborev auto-detects installed agents.
Expand Down
6 changes: 4 additions & 2 deletions cmd/roborev/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ func waitForAnalysisJob(ctx context.Context, serverAddr string, jobID int64) (*s
resp.Body.Close()
return nil, fmt.Errorf("parse job status: %w", err)
}
resp.Body.Close()
_ = resp.Body.Close()

if len(jobsResp.Jobs) == 0 {
return nil, fmt.Errorf("job %d not found", jobID)
Expand Down Expand Up @@ -742,10 +742,12 @@ func runFixAgent(cmd *cobra.Command, repoPath, agentName, model, reasoning, prom
agentName = config.ResolveAgentForWorkflow(agentName, repoPath, cfg, "fix", reasoning)
model = config.ResolveModelForWorkflow(model, repoPath, cfg, "fix", reasoning)

a, err := agent.GetAvailable(agentName)
baseURL := config.ResolveOllamaBaseURL(cfg)
a, err := agent.GetAvailableWithOllamaBaseURL(agentName, baseURL)
if err != nil {
return fmt.Errorf("get agent: %w", err)
}
a = agent.WithOllamaBaseURL(a, baseURL)

// Configure agent: agentic mode, with model and reasoning
reasoningLevel := agent.ParseReasoningLevel(reasoning)
Expand Down
24 changes: 16 additions & 8 deletions cmd/roborev/analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,18 +378,18 @@ func TestWaitForAnalysisJob(t *testing.T) {
switch {
case strings.HasPrefix(r.URL.Path, "/api/jobs"):
if resp.notFound {
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []interface{}{}})
_ = json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []interface{}{}})
return
}
job := storage.ReviewJob{
ID: 42,
Status: storage.JobStatus(resp.status),
Error: resp.errMsg,
}
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []storage.ReviewJob{job}})
_ = json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []storage.ReviewJob{job}})

case strings.HasPrefix(r.URL.Path, "/api/review"):
json.NewEncoder(w).Encode(storage.Review{
_ = json.NewEncoder(w).Encode(storage.Review{
JobID: 42,
Output: resp.review,
})
Expand Down Expand Up @@ -433,7 +433,7 @@ func TestWaitForAnalysisJob_Timeout(t *testing.T) {
ID: 42,
Status: storage.JobStatusQueued,
}
json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []storage.ReviewJob{job}})
_ = json.NewEncoder(w).Encode(map[string]interface{}{"jobs": []storage.ReviewJob{job}})
}))
defer ts.Close()

Expand Down Expand Up @@ -481,7 +481,10 @@ func TestMarkJobAddressed(t *testing.T) {
}

var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("decode request: %v", err)
return
}
gotJobID = int64(req["job_id"].(float64))
gotAddressed = req["addressed"].(bool)

Expand Down Expand Up @@ -513,7 +516,9 @@ func TestMarkJobAddressed(t *testing.T) {

func TestRunFixAgent(t *testing.T) {
tmpDir := t.TempDir()
os.MkdirAll(filepath.Join(tmpDir, ".git"), 0755)
if err := os.MkdirAll(filepath.Join(tmpDir, ".git"), 0755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}

cmd, output := newTestCmd(t)

Expand Down Expand Up @@ -734,7 +739,10 @@ func TestEnqueueAnalysisJob(t *testing.T) {
JobIDStart: 42,
OnEnqueue: func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("decode request: %v", err)
return
}

if req["agentic"] != true {
t.Error("agentic should be true for analysis")
Expand All @@ -744,7 +752,7 @@ func TestEnqueueAnalysisJob(t *testing.T) {
}

w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(storage.ReviewJob{
_ = json.NewEncoder(w).Encode(storage.ReviewJob{
ID: 42,
Agent: "test",
Status: storage.JobStatusQueued,
Expand Down
4 changes: 2 additions & 2 deletions cmd/roborev/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

// writeJSON encodes data as JSON to the response writer.
func writeJSON(w http.ResponseWriter, data interface{}) {
json.NewEncoder(w).Encode(data)
_ = json.NewEncoder(w).Encode(data)
}

// mockJobsHandler returns an http.HandlerFunc that filters the given jobs
Expand Down Expand Up @@ -305,7 +305,7 @@ func TestFindJobForCommit(t *testing.T) {
repoDir := t.TempDir()
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("not json"))
_, _ = w.Write([]byte("not json"))
}))
defer cleanup()

Expand Down
7 changes: 5 additions & 2 deletions cmd/roborev/comment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ func TestCommentJobFlag(t *testing.T) {
var req struct {
JobID int64 `json:"job_id"`
}
json.NewDecoder(r.Body).Decode(&req)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("decode request: %v", err)
return
}
receivedJobID = req.JobID
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(storage.Response{ID: 1, JobID: &receivedJobID})
_ = json.NewEncoder(w).Encode(storage.Response{ID: 1, JobID: &receivedJobID})
return
}
}))
Expand Down
4 changes: 3 additions & 1 deletion cmd/roborev/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,12 @@ func resolveFixAgent(repoPath string, opts fixOptions) (agent.Agent, error) {
agentName := config.ResolveAgentForWorkflow(opts.agentName, repoPath, cfg, "fix", reasoning)
modelStr := config.ResolveModelForWorkflow(opts.model, repoPath, cfg, "fix", reasoning)

a, err := agent.GetAvailable(agentName)
baseURL := config.ResolveOllamaBaseURL(cfg)
a, err := agent.GetAvailableWithOllamaBaseURL(agentName, baseURL)
if err != nil {
return nil, fmt.Errorf("get agent: %w", err)
}
a = agent.WithOllamaBaseURL(a, baseURL)

reasoningLevel := agent.ParseReasoningLevel(reasoning)
a = a.WithAgentic(true).WithReasoning(reasoningLevel)
Expand Down
Loading
Loading